diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 1884aa9..0398f9f 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -35,6 +35,7 @@ kotlin { implementation(compose.preview) implementation(libs.androidx.activity.compose) implementation(libs.kotlinx.coroutines.android) + implementation(libs.play.services.location) implementation(libs.play.services.maps) implementation(libs.maps.compose) } @@ -49,6 +50,8 @@ kotlin { implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.kotlinx.coroutines.core) + implementation(libs.moko.geo) + implementation(libs.moko.geo.compose) implementation(projects.shared) } @@ -89,3 +92,8 @@ dependencies { secrets { propertiesFileName = "secrets.properties" } + +compose.resources { + publicResClass = true + packageOfResClass = "moe.lava.banksia.resources" +} diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 6535982..d1aacd5 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -2,6 +2,8 @@ + + , polylines: List, - cameraPosition: Point, - sheetState: SheetState, + newCameraPosition: Point?, + cameraPositionUpdated: () -> Unit, + extInsets: Int, ) { - val extInsets = if ( - sheetState.currentValue != SheetValue.Hidden || - sheetState.targetValue != SheetValue.Hidden - ) { - val context = LocalContext.current - val windowManager = remember { context.getSystemService(Context.WINDOW_SERVICE) as WindowManager } - val screenHeight = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - windowManager.currentWindowMetrics.bounds.height() - else { - var outMetrics = DisplayMetrics() - @Suppress("DEPRECATION") - windowManager.defaultDisplay.getMetrics(outMetrics) - outMetrics.heightPixels - } - val scaffoldOffset = sheetState.requireOffset().roundToInt() - (screenHeight - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0) - } else 0 var camPos = rememberCameraPositionState() - LaunchedEffect(cameraPosition) { - camPos.position = CameraPosition(cameraPosition.toLatLng(), 16.0f, 0.0f, 0.0f) + val ctx = LocalContext.current + val fusedLocation = remember { LocationServices.getFusedLocationProviderClient(ctx) } + LaunchedEffect(Unit) { + fusedLocation.lastLocation.addOnSuccessListener { + if (it != null) + camPos.position = CameraPosition(LatLng(it.latitude, it.longitude), 16.0f, 0.0f, 0.0f) + } + } + LaunchedEffect(newCameraPosition) { + if (newCameraPosition != null) { + camPos.position = CameraPosition(newCameraPosition.toLatLng(), 16.0f, 0.0f, 0.0f) + cameraPositionUpdated() + } } GoogleMap( @@ -70,8 +75,8 @@ actual fun Maps( cameraPositionState = camPos, mapColorScheme = ComposeMapColorScheme.FOLLOW_SYSTEM, properties = DefaultMapProperties.copy( - //mapStyleOptions = MapStyleOptions.loadRawResourceStyle(LocalContext.current, R.raw.def_mapstyle), - //isMyLocationEnabled = checkLocationPermission(), + mapStyleOptions = MapStyleOptions.loadRawResourceStyle(LocalContext.current, R.raw.def_mapstyle), + isMyLocationEnabled = checkLocationPermission(), ), uiSettings = DefaultMapUiSettings.copy( zoomControlsEnabled = false, diff --git a/composeApp/src/androidMain/res/raw/def_mapstyle.json b/composeApp/src/androidMain/res/raw/def_mapstyle.json new file mode 100644 index 0000000..3227b03 --- /dev/null +++ b/composeApp/src/androidMain/res/raw/def_mapstyle.json @@ -0,0 +1,37 @@ +[ + { + "featureType": "poi.business", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.icon", + "stylers": [ + { + "visibility": "off" + } + ] + }, + { + "featureType": "transit", + "elementType": "labels.icon", + "stylers": [ + { + "visibility": "off" + } + ] + } +] diff --git a/composeApp/src/commonMain/composeResources/drawable/my_location_24.xml b/composeApp/src/commonMain/composeResources/drawable/my_location_24.xml new file mode 100644 index 0000000..683a624 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/my_location_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt index 0d1e4f8..85ebfe9 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt @@ -1,9 +1,18 @@ package moe.lava.banksia import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.safeContent +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetScaffoldState @@ -12,12 +21,26 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +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 import moe.lava.banksia.api.ptv.PtvService import moe.lava.banksia.native.maps.Maps +import moe.lava.banksia.native.maps.Point +import moe.lava.banksia.native.maps.getScreenHeight +import moe.lava.banksia.resources.Res +import moe.lava.banksia.resources.my_location_24 import moe.lava.banksia.ui.Searcher +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview +import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -30,17 +53,43 @@ fun App() { ) ) + val locationFactory = rememberLocationTrackerFactory(LocationTrackerAccuracy.Best) + 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 searchTextState by remember { mutableStateOf("") } var searchExpandedState by remember { mutableStateOf(false) } + val sheetState = scaffoldState.bottomSheetState + val extInsets = if ( + sheetState.currentValue != SheetValue.Hidden || + sheetState.targetValue != SheetValue.Hidden + ) { + val scaffoldOffset = sheetState.requireOffset().roundToInt() + (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) + } + } + MaterialTheme { BottomSheetScaffold( scaffoldState = scaffoldState, + modifier = Modifier.fillMaxSize(), sheetContent = { Box(modifier = Modifier) }, ) { Maps( modifier = Modifier.fillMaxSize(), - sheetState = scaffoldState.bottomSheetState, + newCameraPosition = newCameraPosition, + cameraPositionUpdated = { newCameraPosition = null }, + extInsets = extInsets, ) Searcher( ptvService = PtvService(), @@ -50,6 +99,20 @@ fun App() { onTextChange = { searchTextState = it }, onRouteChange = {} ) + + Box( + Modifier.windowInsetsPadding(WindowInsets.safeContent.add(WindowInsets(bottom = extInsets))), + contentAlignment = Alignment.BottomEnd + ) { + FloatingActionButton( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + onClick = { + newCameraPosition = lastLocation + }, + ) { + 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 e6ce273..4d2b421 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 @@ -5,17 +5,22 @@ import androidx.compose.material3.SheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp data class Marker(val name: String, val onClick: () -> Boolean) data class Point(val lat: Double, val lng: Double) data class Polyline(val points: List, val colour: Color) +@Composable +expect fun getScreenHeight(): Int + @OptIn(ExperimentalMaterial3Api::class) @Composable expect fun Maps( modifier: Modifier = Modifier, markers: List = listOf(), polylines: List = listOf(), - cameraPosition: Point = Point(-37.8136, 144.9631), - sheetState: SheetState, + newCameraPosition: Point? = Point(-37.8136, 144.9631), + cameraPositionUpdated: () -> Unit, + extInsets: Int, ) 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 474559a..5a216d9 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 @@ -3,7 +3,16 @@ package moe.lava.banksia.native.maps import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetState import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.unit.Dp + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun getScreenHeight(): Int { + return LocalWindowInfo.current.containerSize.height +} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -11,8 +20,9 @@ actual fun Maps( modifier: Modifier, markers: List, polylines: List, - cameraPosition: Point, - sheetState: SheetState, + newCameraPosition: Point?, + cameraPositionUpdated: () -> Unit, + extInsets: Int, ) { TODO("Not yet implemented") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 93a725f..d269c9d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ androidx-material = "1.12.0" androidx-test-junit = "1.2.1" compose-multiplatform = "1.7.3" coroutines = "1.9.0" +geo = "0.8.0" junit = "4.13.2" kotlin = "2.1.10" kotlinxSerializationJson = "1.8.1" @@ -20,10 +21,13 @@ ktor = "3.1.1" logback = "1.5.17" mapsCompose = "6.4.1" okio = "3.11.0" +playServicesLocation = "21.3.0" playServicesMaps = "19.1.0" secretsGradlePlugin = "2.0.1" [libraries] +moko-geo = { module = "dev.icerock.moko:geo", version.ref = "geo" } +moko-geo-compose = { module = "dev.icerock.moko:geo-compose", version.ref = "geo" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -50,6 +54,7 @@ ktor-server-tests = { module = "io.ktor:ktor-server-test-host", version.ref = "k logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } +play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" } secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }