diff --git a/composeApp/src/androidMain/kotlin/moe/lava/banksia/native/maps/Maps.android.kt b/composeApp/src/androidMain/kotlin/moe/lava/banksia/native/maps/Maps.android.kt index 446e475..2ba3497 100644 --- a/composeApp/src/androidMain/kotlin/moe/lava/banksia/native/maps/Maps.android.kt +++ b/composeApp/src/androidMain/kotlin/moe/lava/banksia/native/maps/Maps.android.kt @@ -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, polylines: List, - newCameraPosition: Point?, + newCameraPosition: Pair?>?, 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() } } diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt index 5e2d1d9..7ed1423 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt @@ -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): Pair { + 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(-37.8136, 144.9631)) } + var newCameraPosition by remember { + mutableStateOf?>?>( + 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(null) } + val polylines = remember { mutableStateListOf() } + + 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() + 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") diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/native/maps/Maps.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/native/maps/Maps.kt index 40c0b53..b2afb24 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/native/maps/Maps.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/native/maps/Maps.kt @@ -18,7 +18,8 @@ expect fun Maps( modifier: Modifier = Modifier, markers: List = listOf(), polylines: List = listOf(), - newCameraPosition: Point? = Point(-37.8136, 144.9631), + // > + newCameraPosition: Pair?>? = Pair(Point(-37.8136, 144.9631), null), cameraPositionUpdated: () -> Unit, extInsets: Int, ) diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt index a2c65a1..2cc82f6 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt @@ -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) } diff --git a/composeApp/src/iosMain/kotlin/moe/lava/banksia/native/maps/Maps.ios.kt b/composeApp/src/iosMain/kotlin/moe/lava/banksia/native/maps/Maps.ios.kt index 3939a22..eb20020 100644 --- a/composeApp/src/iosMain/kotlin/moe/lava/banksia/native/maps/Maps.ios.kt +++ b/composeApp/src/iosMain/kotlin/moe/lava/banksia/native/maps/Maps.ios.kt @@ -18,7 +18,7 @@ actual fun Maps( modifier: Modifier, markers: List, polylines: List, - newCameraPosition: Point?, + newCameraPosition: Pair?>?, cameraPositionUpdated: () -> Unit, extInsets: Int, ) { diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/PtvService.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/PtvService.kt index f5e4319..652b0ba 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/PtvService.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/PtvService.kt @@ -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) + data class Route(val route: moe.lava.banksia.api.ptv.structures.Route) + @Serializable + data class Routes(val routes: List) } 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 { - val response: Responses.Route = client.get("routes").body() + val response: Responses.Routes = client.get("routes").body() return response.routes } } diff --git a/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/structures/Route.kt b/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/structures/Route.kt index 00686c4..8cfe330 100644 --- a/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/structures/Route.kt +++ b/shared/src/commonMain/kotlin/moe/lava/banksia/api/ptv/structures/Route.kt @@ -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, +) + @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, ) { 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 + } }