MapView, Navigation to RecentPlace

This commit is contained in:
Dimitris
2025-12-01 19:45:17 +01:00
parent da209a4354
commit cddb193260
16 changed files with 346 additions and 154 deletions

View File

@@ -10,7 +10,7 @@ android {
defaultConfig {
applicationId = "com.kouros.navigation"
minSdk = 30
minSdk = 33
targetSdk = 36
versionCode = 1
versionName = "0.1.3"
@@ -46,11 +46,11 @@ android {
// Specifies one flavor dimension.
flavorDimensions += "version"
productFlavors {
create("demo") {
dimension = "version"
applicationIdSuffix = ".demo"
versionNameSuffix = "-demo"
}
// create("demo") {
// dimension = "version"
// applicationIdSuffix = ".demo"
// versionNameSuffix = "-demo"
// }
create("full") {
dimension = "version"
applicationIdSuffix = ".full"

View File

@@ -9,6 +9,7 @@ android {
compileSdk = 36
defaultConfig {
minSdk = 33
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}

View File

@@ -1,8 +1,10 @@
package com.kouros.navigation.car
import android.location.Location
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Badge
@@ -11,13 +13,12 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
@@ -26,6 +27,7 @@ import androidx.compose.ui.unit.sp
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
@@ -134,6 +136,7 @@ fun NavigationImage(height: Int, street: String) {
}
) {
Icon(
modifier = Modifier.size(72.dp, 72.dp),
imageVector = vector,
contentDescription = "Navigation",
tint = color
@@ -142,35 +145,69 @@ fun NavigationImage(height: Int, street: String) {
Text(text = street)
}
}
@Composable
private fun Speed(
width: Int,
height: Int,
location: Location
) {
val textMeasurer = rememberTextMeasurer()
val radius = 32
Box(
modifier = Modifier
.size(30.dp, 30.dp)
.padding(
start = width.dp - percent(width, 20).dp,
top = height.dp - 60.dp
start = width.dp- 300.dp,
top = height.dp- 80.dp
),
contentAlignment = Alignment.Center
) {
val textMeasurerSpeed = rememberTextMeasurer()
val textMeasurerKm = rememberTextMeasurer()
val speed = (location.speed * 3.6).toInt().toString()
val kmh = "km/h"
val styleSpeed = TextStyle(
fontSize = 22.sp,
color = Color.White,
)
.drawWithCache {
val measuredText =
textMeasurer.measure(
AnnotatedString("${(location.speed * 3.6).toInt()}"),
style = TextStyle(color = Color.White, fontSize = 22.sp)
val styleKm = TextStyle(
fontSize = 12.sp,
color = Color.White,
)
onDrawBehind {
val textLayoutSpeed = remember(speed) {
textMeasurerSpeed.measure(speed, styleSpeed)
}
val textLayoutKm = remember(kmh) {
textMeasurerSpeed.measure(kmh, styleKm)
}
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(
Color.Black, radius = 30.dp.toPx(), center = Offset(15f, 12f)
center = Offset(
x = center.x,
y = center.y
),
radius = radius.toFloat(),
color = SpeedColor,
)
drawText(
textMeasurer = textMeasurerSpeed,
text = speed,
style = styleSpeed,
topLeft = Offset(
x = center.x - textLayoutSpeed.size.width / 2,
y = center.y - textLayoutSpeed.size.height / 2 - 5,
)
)
drawText(
textMeasurer = textMeasurerKm,
text = "km/h",
style = styleKm,
topLeft = Offset(
x = center.x - textLayoutKm.size.width / 2,
y = center.y - textLayoutKm.size.height / 2 + 15,
)
)
drawText(measuredText)
}
}
)
}
fun getPaddingValues(height: Int, preView: Boolean): PaddingValues {

View File

@@ -87,7 +87,6 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lifecycle.addObserver(mLifeCycleObserver)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onCreateScreen(intent: Intent): Screen {
routeModel = RouteCarModel()

View File

@@ -54,7 +54,7 @@ class SurfaceRenderer(
)
)
var visibleArea = MutableLiveData(
Rect()
Rect(0,0,0,0)
)
var stableArea = Rect()
@@ -76,6 +76,7 @@ class SurfaceRenderer(
var panView = false
val tilt = 55.0
var previewDistance = 0.0
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
lateinit var lifecycleOwner: CustomLifecycleOwner
@@ -84,7 +85,6 @@ class SurfaceRenderer(
lateinit var presentation: Presentation
@RequiresApi(Build.VERSION_CODES.M)
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
synchronized(this@SurfaceRenderer) {
Log.i(TAG, "Surface available $surfaceContainer")
@@ -134,7 +134,6 @@ class SurfaceRenderer(
}
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
synchronized(this@SurfaceRenderer) {
Log.i(TAG, "SurfaceRenderer destroyed")
@@ -307,25 +306,29 @@ class SurfaceRenderer(
previewRouteData.value = routeModel.route.routeGeoJson
centerLocation = routeModel.centerLocation
preview = true
previewDistance = routeModel.route.distance
}
private fun previewZoom(): Double {
if (routeModel.isNavigating()) {
when (routeModel.route.distance) {
when (previewDistance) {
in 0.0..10.0 -> {
return 14.0
return 13.0
}
in 10.0..20.0 -> {
return 12.0
}
in 20.0..30.0 -> {
return 11.0
}
}
}
in 20.0..30.0 -> {
return 10.0
}
}
return 9.0
}
fun setPreViewDistance(): Double {
return previewDistance
}
companion
object {
private const val TAG = "MapRenderer"

View File

@@ -6,7 +6,6 @@ import android.location.Location
import android.location.LocationManager
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
@@ -14,8 +13,12 @@ import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo
@@ -31,6 +34,7 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location
class NavigationScreen(
carContext: CarContext,
@@ -49,40 +53,39 @@ class NavigationScreen(
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
lateinit var recentPlace: Place
var recentPlaceFound = false
var recentPlaceActive = true
var calculateNewRoute = false
val vieModel = ViewModel(NavigationRepository())
val viewModel = ViewModel(NavigationRepository())
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
routeModel.startNavigation(route)
surfaceRenderer.setRouteData()
recentPlaceActive = false
invalidate()
}
}
val recentObserver = Observer<Place> { lastPlace ->
if (!routeModel.isNavigating()) {
recentPlace = lastPlace
recentPlaceFound = true
invalidate()
}
}
init {
vieModel.route.observe(this, observer)
viewModel.route.observe(this, observer)
viewModel.recentPlace.observe(this, recentObserver)
viewModel.loadRecentPlace(location = surfaceRenderer.lastLocation)
}
override fun onGetTemplate(): Template {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.ic_search_black36dp))
.setOnClickListener {
startSearchScreen()
}
//.setFlags(Action.FLAG_IS_PERSISTENT)
.build()
)
actionStripBuilder.addAction(
Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_24px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext))
}
//.setFlags(Action.FLAG_IS_PERSISTENT)
.build()
)
val actionStripBuilder = createActionStripBuilder()
return if (routeModel.isNavigating()) {
if (calculateNewRoute) {
getNavigationLoadingTemplate(actionStripBuilder)
@@ -96,21 +99,7 @@ class NavigationScreen(
private fun getNavigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
actionStripBuilder.addAction(
Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_close_white_24dp
)
)
.build()
)
.setOnClickListener {
stopNavigation()
}
.build()
stopAction()
)
return NavigationTemplate.Builder()
.setNavigationInfo(
@@ -123,7 +112,7 @@ class NavigationScreen(
.build()
}
private fun getNavigationEndTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
private fun getNavigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.isArrived()) {
val timer = object : CountDownTimer(10000, 10000) {
override fun onTick(millisUntilFinished: Long) {}
@@ -155,13 +144,42 @@ class NavigationScreen(
.setMapActionStrip(mapActionStripBuilder().build())
.build()
} else {
return NavigationTemplate.Builder()
return if (recentPlaceFound && recentPlaceActive) {
return getRecentPlaceTemplate()
} else {
NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.build()
}
}
}
fun getRecentPlaceTemplate(): Template {
val messageTemplate = MessageTemplate.Builder(
recentPlace.name + "\n"
+ recentPlace.city
)
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.drive_now))
.build()
)
.addAction(navigateAction())
.addAction(closeAction())
.build()
val builder = MapWithContentTemplate.Builder()
.setContentTemplate(messageTemplate)
.setActionStrip(
mapActionStripBuilder()
.addAction(settingsAction())
.addAction(searchAction())
.build()
)
return builder.build()
}
fun getNavigationLoadingTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
return NavigationTemplate.Builder()
@@ -198,11 +216,105 @@ class NavigationScreen(
}
}
private fun createActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
searchAction()
)
actionStripBuilder.addAction(
settingsAction()
)
return actionStripBuilder
}
private fun mapActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus())
.addAction(zoomMinus())
if (surfaceRenderer.panView) {
actionStripBuilder
.addAction(
Action.Builder()
panAction()
)
}
return actionStripBuilder
}
private fun stopAction(): Action {
return Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_close_white_24dp
)
)
.build()
)
.setOnClickListener {
stopNavigation()
}
.build()
}
private fun navigateAction(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.assistant_navigation_48px
)
)
.build()
)
.setOnClickListener {
val navigateTo = location(recentPlace.latitude, recentPlace.longitude)
viewModel.loadRoute(surfaceRenderer.lastLocation, navigateTo)
routeModel.destination = recentPlace
}
.build()
}
private fun closeAction(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_close_white_24dp
)
)
.build()
)
.setOnClickListener {
recentPlaceActive = false
invalidate()
}
.build()
}
private fun searchAction(): Action {
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.ic_search_black36dp))
.setOnClickListener {
startSearchScreen()
}
.build()
}
private fun settingsAction(): Action {
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_24px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext))
}
.build()
}
private fun zoomPlus(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
@@ -215,9 +327,10 @@ class NavigationScreen(
surfaceRenderer.handleScale(1)
}
.build()
)
.addAction(
Action.Builder()
}
private fun zoomMinus(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
@@ -229,10 +342,11 @@ class NavigationScreen(
).setOnClickListener {
surfaceRenderer.handleScale(-1)
}
.build())
if (surfaceRenderer.panView) {
actionStripBuilder.addAction(
Action.Builder()
.build()
}
private fun panAction(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
@@ -245,9 +359,6 @@ class NavigationScreen(
surfaceRenderer.panView = false
}
.build()
)
}
return actionStripBuilder
}
private fun getSuggestion(title: Int, subtitle: Int, icon: CarIcon): Suggestion {
@@ -282,8 +393,8 @@ class NavigationScreen(
val location = Location(LocationManager.GPS_PROVIDER)
location.latitude = place.latitude
location.longitude = place.longitude
vieModel.saveRecent(place)
vieModel.loadRoute(surfaceRenderer.lastLocation, location)
viewModel.saveRecent(place)
viewModel.loadRoute(surfaceRenderer.lastLocation, location)
currentNavigationLocation = location
routeModel.destination = place
invalidate()
@@ -314,7 +425,7 @@ class NavigationScreen(
fun reRoute() {
NavigationMessage(carContext).createAlert()
vieModel.loadRoute(surfaceRenderer.lastLocation, currentNavigationLocation)
viewModel.loadRoute(surfaceRenderer.lastLocation, currentNavigationLocation)
}
fun updateTrip() {

View File

@@ -2,9 +2,11 @@ package com.kouros.navigation.car.screen
import android.location.Location
import android.net.Uri
import android.os.Build
import android.text.Spannable
import android.text.SpannableString
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen

View File

@@ -15,7 +15,6 @@
*/
package com.kouros.navigation.car.screen
import android.location.Geocoder
import android.location.Location
import android.location.LocationManager
import android.os.CountDownTimer
@@ -60,7 +59,6 @@ class RoutePreviewScreen(
private var mItemLimit = 0
private var street = ""
val vieModel = ViewModel(NavigationRepository())
val routeModel = RouteCarModel()
@@ -70,16 +68,9 @@ class RoutePreviewScreen(
if (route.isNotEmpty()) {
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
val geocoder = Geocoder(carContext)
// nominatim ->
geocoder.getFromLocation(destination.latitude, destination.longitude, 1) {
for (address in it) {
street = address.getAddressLine(0)
}
invalidate()
}
}
}
init {
vieModel.previewRoute.observe(this, observer)
@@ -192,7 +183,7 @@ class RoutePreviewScreen(
return Row.Builder()
.setTitle(route)
.setOnClickListener { onRouteSelected(index) }
.addText(street)
.addText( "${destination.street!!} ${destination.postalCode} ${destination.city}")
.addAction(action)
.build()
}

View File

@@ -37,7 +37,6 @@ class SearchScreen(
lateinit var searchResult: List<SearchResult>
val observer = Observer<List<SearchResult>> { newSearch ->
println(newSearch)
searchResult = newSearch
invalidate()
}

View File

@@ -364,4 +364,5 @@
<string name="route_preview">Routen</string>
<string name="recent_destinations">Letzte Ziele</string>
<string name="contacts">Kontakte</string>
<string name="drive_now">Jetzt losfahren</string>
</resources>

View File

@@ -491,4 +491,5 @@
<string name="route_preview">Routes</string>
<string name="recent_destinations">Recent</string>
<string name="contacts">Contacts</string>
<string name="drive_now">Drive now</string>
</resources>

View File

@@ -15,6 +15,7 @@ android {
compileSdk = 36
defaultConfig {
minSdk = 33
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}

View File

@@ -5,3 +5,5 @@ import androidx.compose.ui.graphics.Color
val NavigationColor = Color(0xFF052086)
val RouteColor = Color(0xFF5582D0)
val SpeedColor = Color(0xFF262525)

View File

@@ -32,7 +32,8 @@ class NavigationRepository {
private val routeUrl = "https://kouros-online.de/valhalla/route?json="
private val nominatimUrl = "https://nominatim.openstreetmap.org/search?q="
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
fun getRoute(currentLocation : Location, location: Location): String {
val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude),
@@ -57,7 +58,11 @@ class NavigationRepository {
}
fun searchPlaces(search : String) : String {
return fetchUrl("$nominatimUrl$search&format=jsonv2&addressdetails=true&countrycodes=de", false)
return fetchUrl("${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true&countrycodes=de", false)
}
fun reverseAddress(location: Location) : String {
return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true&countrycodes=de", false)
}
fun getPlaces(): List<Place> {
@@ -103,6 +108,7 @@ class NavigationRepository {
httpURLConnection.setRequestProperty("User-Agent", "email=nominatim@kouros-online.de");
httpURLConnection.requestMethod = "GET"
val responseCode = httpURLConnection.responseCode
println(responseCode)
if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader()
.use { it.readText() } // defaults to UTF-8

View File

@@ -16,8 +16,8 @@ data class SearchResult(
@SerializedName("osm_type") var osmType: String = "",
@SerializedName("osm_id") var osmId: Long = 0,
@SerializedName("lat") var lat: String = "",
@SerializedName("lon") var lon: String = "",
@SerializedName("category") var category: String = "",
@SerializedName("lon") var lon: String = "",
@SerializedName("type") var type: String = "",
@SerializedName("place_rank") var placeRank: Int = 0,
@SerializedName("importance") var importance: Double = 0.0,

View File

@@ -3,6 +3,8 @@ package com.kouros.navigation.model
import android.content.Context
import android.location.Geocoder
import android.location.Location
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -32,6 +34,9 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData<String>()
}
val recentPlace: MutableLiveData<Place> by lazy {
MutableLiveData<Place>()
}
val places: MutableLiveData<List<Place>> by lazy {
MutableLiveData<List<Place>>()
}
@@ -45,6 +50,31 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
fun loadRecentPlace(location: Location) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
val query = placeBox
.query(Place_.name.notEqual(""))
.orderDesc(Place_.lastDate)
.build()
val results = query.find()
query.close()
for (place in results) {
val plLocation = location(place.latitude, place.longitude)
// val distance = repository.getRouteDistance(location, plLocation)
//place.distance = distance.toFloat()
if (place.distance == 0F) {
recentPlace.postValue(place)
println("RecentPlace $recentPlace")
return@launch
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun loadPlaces(location: Location) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -59,7 +89,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val plLocation = location(place.latitude, place.longitude)
val distance = repository.getRouteDistance(location, plLocation)
place.distance = distance.toFloat()
println(place.lastDate)
}
places.postValue(results)
} catch (e: Exception) {
@@ -148,6 +177,15 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun reverseAddress(location: Location ): String {
val address = repository.reverseAddress(location)
println(address)
val gson = GsonBuilder().serializeNulls().create()
val place = gson.fromJson(address, SearchResult::class.java)
println(place.address.road)
return place.address.road
}
fun saveRecent(place: Place) {
viewModelScope.launch(Dispatchers.IO) {
place.category = Constants.RECENT