Compare commits

...

3 Commits

Author SHA1 Message Date
Dimitris
fdf2ee9f48 CarInfo and CarSensors Osrm 2026-01-03 14:04:50 +01:00
Dimitris
1eab4f1aa3 CarInfo and CarSensors 2025-12-31 11:16:41 +01:00
Dimitris
9f356bd728 Osrm 2025-12-30 16:11:30 +01:00
52 changed files with 654 additions and 277 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 15 versionCode = 18
versionName = "0.1.3.15" versionName = "0.1.3.18"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>--> <!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>-->
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="MockLocation" /> tools:ignore="MockLocation" />
@@ -20,6 +20,7 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Navigation"> android:theme="@style/Theme.Navigation">
<meta-data <meta-data

View File

@@ -2,25 +2,14 @@ package com.kouros.navigation
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.di.appModule import com.kouros.navigation.di.appModule
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.logger.Level import org.koin.core.logger.Level
import org.maplibre.compose.expressions.dsl.switch
class MainApplication : Application() { class MainApplication : Application() {
@@ -28,7 +17,6 @@ class MainApplication : Application() {
super.onCreate() super.onCreate()
ObjectBox.init(this); ObjectBox.init(this);
appContext = applicationContext appContext = applicationContext
setIntKeyValue(appContext!!, RouteEngine.VALHALLA.ordinal, ROUTE_ENGINE)
navigationViewModel = getRouteEngine(appContext!!) navigationViewModel = getRouteEngine(appContext!!)
startKoin { startKoin {
androidLogger(Level.DEBUG) androidLogger(Level.DEBUG)
@@ -42,8 +30,6 @@ class MainApplication : Application() {
var appContext: Context? = null var appContext: Context? = null
private set private set
var useContacts = false
lateinit var navigationViewModel : ViewModel lateinit var navigationViewModel : ViewModel
} }
} }

View File

@@ -3,13 +3,15 @@ package com.kouros.navigation.di
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module import org.koin.dsl.module
import kotlin.math.sin
val appModule = module { val appModule = module {
viewModelOf(::ViewModel) viewModelOf(::ViewModel)
singleOf(::ValhallaRepository) singleOf(::ValhallaRepository)
singleOf(::OsrmRepository) singleOf(::OsrmRepository)
} }

View File

@@ -39,20 +39,21 @@ import androidx.lifecycle.Observer
import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import com.kouros.navigation.MainApplication.Companion.navigationViewModel import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.homeLocation import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.MockLocation import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
@@ -60,6 +61,8 @@ import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.Location import org.maplibre.compose.location.Location
import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.style.BaseStyle
import org.maplibre.geojson.Point
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -101,6 +104,8 @@ class MainActivity : ComponentActivity() {
private var overpass = false private var overpass = false
lateinit var baseStyle: BaseStyle.Json
init { init {
navigationViewModel.route.observe(this, observer) navigationViewModel.route.observe(this, observer)
} }
@@ -108,13 +113,15 @@ class MainActivity : ComponentActivity() {
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]) @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val darkModeSettings = getIntKeyValue(applicationContext, Constants.DARK_MODE_SETTINGS)
baseStyle = BaseStyleModel().readStyle(applicationContext, darkModeSettings, false)
if (useMock) { if (useMock) {
checkMockLocationEnabled() checkMockLocationEnabled()
} }
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.lastLocation fusedLocationClient.lastLocation
.addOnSuccessListener { location : android.location.Location? -> .addOnSuccessListener { location: android.location.Location? ->
if (useMock) { if (useMock) {
mock = MockLocation(locationManager) mock = MockLocation(locationManager)
mock.setMockLocation( mock.setMockLocation(
@@ -196,7 +203,8 @@ class MainActivity : ComponentActivity() {
step, step,
cameraPosition, cameraPosition,
routeData, routeData,
tilt tilt,
baseStyle
) )
} }
} }
@@ -229,6 +237,7 @@ class MainActivity : ComponentActivity() {
&& lastLocation.longitude != location.position.longitude && lastLocation.longitude != location.position.longitude
) { ) {
val currentLocation = location(location.position.longitude, location.position.latitude) val currentLocation = location(location.position.longitude, location.position.latitude)
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
with(routeModel) { with(routeModel) {
if (isNavigating()) { if (isNavigating()) {
updateLocation(currentLocation, navigationViewModel) updateLocation(currentLocation, navigationViewModel)
@@ -245,7 +254,6 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
val zoom = calculateZoom(location.speed) val zoom = calculateZoom(location.speed)
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
@@ -299,12 +307,11 @@ class MainActivity : ComponentActivity() {
fun simulate() { fun simulate() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
for ((index, step) in routeModel.legs.steps.withIndex()) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
mock.setMockLocation(waypoint[1], waypoint[0]) for ((index, waypoint) in routeModel.route.waypoints!!.withIndex()) {
delay(800L) // var deviation = 0.0
} mock.setMockLocation(waypoint[1] + deviation, waypoint[0])
delay(500L) //
} }
} }
} }

View File

@@ -7,17 +7,14 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.map.DarkMode
import com.kouros.navigation.car.map.MapLibre import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.NavigationImage import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.data.Constants import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
@@ -34,10 +31,12 @@ fun MapView(
step: StepData?, step: StepData?,
cameraPosition: MutableLiveData<CameraPosition>, cameraPosition: MutableLiveData<CameraPosition>,
routeData: MutableLiveData<String>, routeData: MutableLiveData<String>,
tilt: Double tilt: Double,
baseStyle: BaseStyle.Json,
) { ) {
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(applicationContext) val metrics =
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(applicationContext)
val width = metrics.bounds.width() val width = metrics.bounds.width()
val height = metrics.bounds.height() val height = metrics.bounds.height()
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp) val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
@@ -55,17 +54,15 @@ fun MapView(
zoom = 15.0, zoom = 15.0,
) )
) )
val baseStyle = remember {
mutableStateOf(BaseStyle.Uri(Constants.STYLE)) val rememberBaseStyle = rememberBaseStyle( baseStyle)
}
DarkMode(applicationContext, baseStyle)
Column { Column {
NavigationInfo(step) NavigationInfo(step)
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
MapLibre( MapLibre(
applicationContext, applicationContext,
cameraState, cameraState,
baseStyle, rememberBaseStyle,
route, route,
ViewStyle.VIEW ViewStyle.VIEW
) )

View File

@@ -46,6 +46,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
implementation(libs.androidx.app.projected)
//implementation(libs.maplibre.composeMaterial3) //implementation(libs.maplibre.composeMaterial3)
implementation(project(":common:data")) implementation(project(":common:data"))

View File

@@ -32,8 +32,11 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" /> <uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED"/>
<uses-permission android:name="android.car.permission.READ_CAR_DISPLAY_UNITS"/>
<application android:requestLegacyExternalStorage="true"> <application android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true">
<meta-data <meta-data
android:name="androidx.car.app.minCarApiLevel" android:name="androidx.car.app.minCarApiLevel"
android:value="1" /> android:value="1" />

View File

@@ -38,9 +38,4 @@ class NavigationCarAppService : CarAppService() {
} }
} }
public interface LocationCallback {
fun onLocationChanged(location: Location) {
}
}

View File

@@ -5,6 +5,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.util.Log import android.util.Log
@@ -12,6 +13,12 @@ import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.Session import androidx.car.app.Session
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener
import androidx.car.app.hardware.info.CarHardwareLocation
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Speed
import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationListenerCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
@@ -22,17 +29,19 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.NavigationScreen import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.RequestPermissionScreen import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.ROUTE_ENGINE import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
import org.maplibre.compose.style.BaseStyle
class NavigationSession : Session(), NavigationScreen.Listener { class NavigationSession : Session(), NavigationScreen.Listener {
@@ -45,8 +54,11 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lateinit var surfaceRenderer: SurfaceRenderer lateinit var surfaceRenderer: SurfaceRenderer
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? -> var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
val routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
if (routingEngine == RouteEngine.VALHALLA.ordinal) {
updateLocation(location!!) updateLocation(location!!)
} }
}
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
@@ -66,6 +78,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
override fun onDestroy(owner: LifecycleOwner) { override fun onDestroy(owner: LifecycleOwner) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
carSensors.removeCarHardwareLocationListener(carLocationListener)
carInfo.removeSpeedListener(carSpeedListener)
Log.i(TAG, "In onDestroy()") Log.i(TAG, "In onDestroy()")
val locationManager = val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
@@ -73,24 +89,55 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
} }
lateinit var navigationViewModel : ViewModel lateinit var navigationViewModel: ViewModel
lateinit var baseStyle: BaseStyle.Json
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
OnCarDataAvailableListener { data ->
if (data.location.status == CarValue.STATUS_SUCCESS) {
val location = data.location.value
surfaceRenderer.updateCarLocation(location!!)
}
}
val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
val speed = data.displaySpeedMetersPerSecond.value
surfaceRenderer.updateCarSpeed(speed!!)
}
}
init { init {
val lifecycle: Lifecycle = lifecycle val lifecycle: Lifecycle = lifecycle
lifecycle.addObserver(mLifeCycleObserver) lifecycle.addObserver(mLifeCycleObserver)
} }
fun onRoutingEngineStateUpdated(routeEngine : Int) {
navigationViewModel = when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
else -> ViewModel(OsrmRepository())
}
}
override fun onCreateScreen(intent: Intent): Screen { override fun onCreateScreen(intent: Intent): Screen {
navigationViewModel = getRouteEngine(carContext) navigationViewModel = getRouteEngine(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
routeModel = RouteCarModel() routeModel = RouteCarModel()
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel) val darkMode = getIntKeyValue(carContext, Constants.DARK_MODE_SETTINGS)
baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
navigationScreen = NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, baseStyle)
navigationScreen =
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED && !useContacts == PackageManager.PERMISSION_GRANTED
&& carContext.checkSelfPermission("com.google.android.gms.permission.CAR_SPEED") == PackageManager.PERMISSION_GRANTED
&& !useContacts
|| (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS) || (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED) == PackageManager.PERMISSION_GRANTED)
) { ) {
@@ -111,9 +158,23 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
) )
} }
addSensors()
return navigationScreen return navigationScreen
} }
fun addSensors() {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_NORMAL,
carContext.mainExecutor,
carLocationListener
)
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
val screenManager = carContext.getCarService(ScreenManager::class.java) val screenManager = carContext.getCarService(ScreenManager::class.java)
if ((CarContext.ACTION_NAVIGATE == intent.action)) { if ((CarContext.ACTION_NAVIGATE == intent.action)) {
@@ -151,12 +212,18 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
} }
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
println("Configuration: ${newConfiguration.isNightModeActive}")
super.onCarConfigurationChanged(newConfiguration)
}
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun requestLocationUpdates() { fun requestLocationUpdates() {
val locationManager = val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (location != null) { if (location != null) {
navigationViewModel.loadRecentPlace(location = location, carContext)
updateLocation(location) updateLocation(location)
locationManager.requestLocationUpdates( locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, LocationManager.GPS_PROVIDER,
@@ -169,15 +236,15 @@ class NavigationSession : Session(), NavigationScreen.Listener {
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations()) navigationScreen.updateTrip(location)
val distance = location.distanceTo(snapedLocation) val wayPointLocation = routeModel.route.currentStep().wayPointLocation
val distance = location.distanceTo(wayPointLocation)
if (distance > MAXIMAL_ROUTE_DEVIATION) { if (distance > MAXIMAL_ROUTE_DEVIATION) {
navigationScreen.calculateNewRoute(routeModel.routeState.destination) navigationScreen.calculateNewRoute(routeModel.routeState.destination)
return return
} }
navigationScreen.updateTrip(location)
if (distance < MAXIMAL_SNAP_CORRECTION) { if (distance < MAXIMAL_SNAP_CORRECTION) {
surfaceRenderer.updateLocation(snapedLocation) surfaceRenderer.updateLocation(wayPointLocation)
} else { } else {
surfaceRenderer.updateLocation(location) surfaceRenderer.updateLocation(location)
} }

View File

@@ -5,8 +5,6 @@ import android.graphics.Rect
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay import android.hardware.display.VirtualDisplay
import android.location.Location import android.location.Location
import android.os.CountDownTimer
import android.os.Handler
import android.util.Log import android.util.Log
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
@@ -18,8 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -27,16 +23,18 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.map.DarkMode
import com.kouros.navigation.car.map.DrawNavigationImages import com.kouros.navigation.car.map.DrawNavigationImages
import com.kouros.navigation.car.map.MapLibre import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.cameraState import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.homeLocation import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
@@ -51,7 +49,8 @@ import org.maplibre.spatialk.geojson.Position
class SurfaceRenderer( class SurfaceRenderer(
private var carContext: CarContext, lifecycle: Lifecycle, private var carContext: CarContext, lifecycle: Lifecycle,
private var routeModel: RouteCarModel private var routeModel: RouteCarModel,
private var baseStyle: BaseStyle.Json
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
var lastLocation = location(0.0, 0.0) var lastLocation = location(0.0, 0.0)
@@ -163,7 +162,9 @@ class SurfaceRenderer(
fun onConnectionStateUpdated(connectionState: Int) { fun onConnectionStateUpdated(connectionState: Int) {
when (connectionState) { when (connectionState) {
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext) CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto"
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext) // Automotive OS
} }
} }
@@ -174,12 +175,8 @@ class SurfaceRenderer(
val speedCameras: String? by speedCamerasData.observeAsState() val speedCameras: String? by speedCamerasData.observeAsState()
val paddingValues = getPaddingValues(height, viewStyle) val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt) val cameraState = cameraState(paddingValues, position, tilt)
val rememberBaseStyle = rememberBaseStyle(baseStyle)
val baseStyle = remember { MapLibre(carContext, cameraState, rememberBaseStyle, route, viewStyle, speedCameras)
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
}
DarkMode(carContext, baseStyle)
MapLibre(carContext, cameraState, baseStyle, route, viewStyle, speedCameras)
ShowPosition(cameraState, position, paddingValues) ShowPosition(cameraState, position, paddingValues)
} }
@@ -192,7 +189,7 @@ class SurfaceRenderer(
val cameraDuration = val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing) duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
val currentSpeed: Float? by speed.observeAsState() val currentSpeed: Float? by speed.observeAsState()
if (viewStyle == ViewStyle.VIEW) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
DrawNavigationImages( DrawNavigationImages(
paddingValues, paddingValues,
currentSpeed, currentSpeed,
@@ -262,35 +259,12 @@ class SurfaceRenderer(
) )
lastBearing = cameraPosition.value!!.bearing lastBearing = cameraPosition.value!!.bearing
lastLocation = location lastLocation = location
speed.value = location.speed
if (!countDownTimerActive) {
countDownTimerActive = true
val mainThreadHandler = Handler(carContext.mainLooper)
val lastLocationTimer = lastLocation
checkUpdate(mainThreadHandler, lastLocationTimer)
} }
} }
} }
}
private fun checkUpdate(
mainThreadHandler: Handler,
lastLocationTimer: Location
) {
mainThreadHandler.post {
object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
countDownTimerActive = false
if (lastLocation.time - lastLocationTimer.time < 1500) {
speed.postValue(0F)
}
}
}.start()
}
}
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) { private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
synchronized(this) {
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
bearing = bearing, bearing = bearing,
@@ -301,6 +275,7 @@ class SurfaceRenderer(
) )
) )
} }
}
fun setRouteData() { fun setRouteData() {
routeData.value = routeModel.route.routeGeoJson routeData.value = routeModel.route.routeGeoJson
@@ -322,6 +297,7 @@ class SurfaceRenderer(
} }
fun setCategories(location: Location, route: String) { fun setCategories(location: Location, route: String) {
synchronized(this) {
viewStyle = ViewStyle.AMENITY_VIEW viewStyle = ViewStyle.AMENITY_VIEW
routeData.value = route routeData.value = route
updateCameraPosition( updateCameraPosition(
@@ -330,6 +306,18 @@ class SurfaceRenderer(
target = Position(location.longitude, location.latitude) target = Position(location.longitude, location.latitude)
) )
} }
}
fun updateCarLocation(location: Location) {
val routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
if (routingEngine == RouteEngine.OSRM.ordinal) {
updateLocation(location)
}
}
fun updateCarSpeed(newSpeed: Float) {
speed.value = newSpeed
}
fun setCategoryLocation(location: Location, category: String) { fun setCategoryLocation(location: Location, category: String) {
viewStyle = ViewStyle.AMENITY_VIEW viewStyle = ViewStyle.AMENITY_VIEW

View File

@@ -1,9 +1,8 @@
package com.kouros.navigation.car.map package com.kouros.navigation.car.map
import android.location.Location
import android.content.Context import android.content.Context
import android.location.Location
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -12,13 +11,17 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
@@ -33,9 +36,9 @@ import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.NavigationColor import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
@@ -87,18 +90,19 @@ fun cameraState(
fun MapLibre( fun MapLibre(
context: Context, context: Context,
cameraState: CameraState, cameraState: CameraState,
baseStyle: MutableState<BaseStyle.Uri>, baseStyle: BaseStyle.Json,
route: String?, route: String?,
viewStyle: ViewStyle, viewStyle: ViewStyle,
speedCameras: String? = "" speedCameras: String? = ""
) { ) {
MaplibreMap( MaplibreMap(
options = MapOptions( options = MapOptions(
ornamentOptions = ornamentOptions =
OrnamentOptions(isScaleBarEnabled = false) OrnamentOptions(isScaleBarEnabled = false)
), ),
cameraState = cameraState, cameraState = cameraState,
baseStyle = baseStyle.value baseStyle = baseStyle
) { ) {
getBaseSource(id = "openmaptiles")?.let { tiles -> getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) { if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
@@ -115,6 +119,7 @@ fun MapLibre(
//Puck(cameraState, lastLocation) //Puck(cameraState, lastLocation)
} }
} }
@Composable @Composable
fun RouteLayer(routeData: String?) { fun RouteLayer(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) { if (routeData != null && routeData.isNotEmpty()) {
@@ -172,12 +177,12 @@ fun AmenityLayer(routeData: String?) {
@Composable @Composable
fun SpeedCameraLayer(speedCameras: String?) { fun SpeedCameraLayer(speedCameras: String?) {
if (speedCameras != null && speedCameras.isNotEmpty()) { if (speedCameras != null && speedCameras.isNotEmpty()) {
val color = const(Color.DarkGray) val color = const(Color.Red)
val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras)) val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras))
SymbolLayer( SymbolLayer(
id = "speed-camera-layer", id = "speed-camera-layer",
source = cameraSource, source = cameraSource,
iconImage = image(painterResource(R.drawable.speed_camera_48px), drawAsSdf = true), iconImage = image(painterResource(R.drawable.speed_camera_24px), drawAsSdf = true),
iconColor = color, iconColor = color,
iconSize = iconSize =
interpolate( interpolate(
@@ -191,6 +196,7 @@ fun SpeedCameraLayer(speedCameras: String?) {
) )
} }
} }
@Composable @Composable
fun BuildingLayer(tiles: Source) { fun BuildingLayer(tiles: Source) {
Anchor.Replace("building-3d") { Anchor.Replace("building-3d") {
@@ -367,25 +373,21 @@ private fun MaxSpeed(
} }
@Composable @Composable
fun DarkMode(context: Context, baseStyle: MutableState<BaseStyle.Uri>) { fun rememberBaseStyle( baseStyle : BaseStyle.Json): BaseStyle.Json {
val darkMode = getIntKeyValue(context, Constants.DARK_MODE_SETTINGS) val rememberBaseStyle by remember() {
if (darkMode == 0) { mutableStateOf(baseStyle)
baseStyle.value = BaseStyle.Uri(Constants.STYLE)
}
if (darkMode == 1) {
baseStyle.value = BaseStyle.Uri(Constants.STYLE_DARK)
}
if (darkMode == 2) {
baseStyle.value =
(if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
Constants.STYLE
))
} }
return rememberBaseStyle
} }
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues { fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) { return when (viewStyle) {
ViewStyle.VIEW -> PaddingValues(start = 50.dp, top = distanceFromTop(height).dp) ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(
start = 50.dp,
top = distanceFromTop(height).dp
)
ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp) ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp)
else -> PaddingValues(start = 250.dp, bottom = 0.dp) else -> PaddingValues(start = 250.dp, bottom = 0.dp)
} }

View File

@@ -29,6 +29,8 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
import androidx.car.app.model.DateTimeWithZone import androidx.car.app.model.DateTimeWithZone
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.navigation.model.Lane
import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate import androidx.car.app.navigation.model.TravelEstimate
@@ -46,6 +48,10 @@ class RouteCarModel() : RouteModel() {
val stepData = currentStep() val stepData = currentStep()
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(stepData.instruction) createString(stepData.instruction)
val straightNormal =
Lane.Builder()
.addDirection(LaneDirection.create(LaneDirection.SHAPE_STRAIGHT, false))
.build()
val step = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
.setManeuver( .setManeuver(
@@ -54,12 +60,17 @@ class RouteCarModel() : RouteModel() {
.build() .build()
) )
.setRoad(routeState.destination.street!!) .setRoad(routeState.destination.street!!)
.build() stepData.lane.forEach {
return step if (it.indications.isNotEmpty() ) {
step.setLanesImage(createCarIcon(createLaneIcon(carContext, stepData)))
step.addLane(straightNormal)
}
}
return step.build()
} }
/** Returns the next [Step] with information such as the cue text and images. */ /** Returns the next [Step] with information such as the cue text and images. */
fun nextStep(carContext: CarContext): Step? { fun nextStep(carContext: CarContext): Step {
val stepData = nextStep() val stepData = nextStep()
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(stepData.instruction) createString(stepData.instruction)
@@ -125,6 +136,10 @@ class RouteCarModel() : RouteModel() {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build() return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
} }
fun createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build()
}
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String?) { fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String?) {
carContext.getCarService<AppManager?>(AppManager::class.java) carContext.getCarService<AppManager?>(AppManager::class.java)
.showAlert(createAlert(carContext, distance, maxSpeed)) .showAlert(createAlert(carContext, distance, maxSpeed))

View File

@@ -11,7 +11,6 @@ import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.model.Header import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate import androidx.car.app.model.MessageTemplate
@@ -29,13 +28,11 @@ import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@@ -93,22 +90,17 @@ class NavigationScreen(
var speedCameras = listOf<Elements>() var speedCameras = listOf<Elements>()
val speedObserver = Observer<List<Elements>> { cameras -> val speedObserver = Observer<List<Elements>> { cameras ->
speedCameras = cameras speedCameras = cameras
val coordinates = mutableListOf<List<Double>>() val coordinates = mutableListOf<List<Double>>()
val loc = location(0.0, 0.0)
cameras.forEach { cameras.forEach {
val loc =
location(longitude = it.lon!!, latitude = it.lat!!)
coordinates.add(listOf(it.lon!!, it.lat!!)) coordinates.add(listOf(it.lon!!, it.lat!!))
} }
val speedData = GeoUtils.createPointCollection(coordinates, "radar") val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCamerasData.value =speedData surfaceRenderer.speedCamerasData.value = speedData
} }
init { init {
viewModel.route.observe(this, observer) viewModel.route.observe(this, observer)
viewModel.recentPlace.observe(this, recentObserver) viewModel.recentPlace.observe(this, recentObserver)
viewModel.loadRecentPlace(location = surfaceRenderer.lastLocation)
viewModel.placeLocation.observe(this, placeObserver) viewModel.placeLocation.observe(this, placeObserver)
viewModel.speedCameras.observe(this, speedObserver) viewModel.speedCameras.observe(this, speedObserver)
} }
@@ -240,7 +232,6 @@ class NavigationScreen(
Distance.UNIT_METERS Distance.UNIT_METERS
} }
val nextStep = routeModel.nextStep(carContext = carContext) val nextStep = routeModel.nextStep(carContext = carContext)
if (nextStep != null) {
return RoutingInfo.Builder() return RoutingInfo.Builder()
.setCurrentStep( .setCurrentStep(
routeModel.currentStep(carContext = carContext), routeModel.currentStep(carContext = carContext),
@@ -248,14 +239,6 @@ class NavigationScreen(
) )
.setNextStep(nextStep) .setNextStep(nextStep)
.build() .build()
} else {
return RoutingInfo.Builder()
.setCurrentStep(
routeModel.currentStep(carContext = carContext),
Distance.create(currentDistance, displayUnit)
)
.build()
}
} }
private fun createActionStripBuilder(): ActionStrip.Builder { private fun createActionStripBuilder(): ActionStrip.Builder {
@@ -352,7 +335,7 @@ class NavigationScreen(
return Action.Builder() return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px)) .setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
.setOnClickListener { .setOnClickListener {
screenManager.push(SettingsScreen(carContext)) screenManager.push(SettingsScreen(carContext, viewModel))
} }
.build() .build()
} }
@@ -369,6 +352,7 @@ class NavigationScreen(
.build() .build()
).setOnClickListener { ).setOnClickListener {
surfaceRenderer.handleScale(1) surfaceRenderer.handleScale(1)
invalidate()
} }
.build() .build()
} }
@@ -385,6 +369,7 @@ class NavigationScreen(
.build() .build()
).setOnClickListener { ).setOnClickListener {
surfaceRenderer.handleScale(-1) surfaceRenderer.handleScale(-1)
invalidate()
} }
.build() .build()
} }
@@ -401,6 +386,7 @@ class NavigationScreen(
.build() .build()
).setOnClickListener { ).setOnClickListener {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
} }
.build() .build()
} }
@@ -465,7 +451,7 @@ class NavigationScreen(
} }
fun updateTrip(location: Location) { fun updateTrip(location: Location) {
updateSpeedCamera(location) updateSpeedCamera(surfaceRenderer.lastLocation)
with(routeModel) { with(routeModel) {
updateLocation(location, viewModel) updateLocation(location, viewModel)
if (routeState.maneuverType == Maneuver.TYPE_DESTINATION if (routeState.maneuverType == Maneuver.TYPE_DESTINATION

View File

@@ -12,11 +12,12 @@ import androidx.car.app.model.Toggle
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY import com.kouros.navigation.data.Constants.AVOID_TOLLWAY
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
class NavigationSettings(private val carContext: CarContext) : Screen(carContext) { class NavigationSettings(private val carContext: CarContext, private var viewModel: ViewModel) : Screen(carContext) {
private var motorWayToggleState = false private var motorWayToggleState = false
@@ -52,7 +53,12 @@ class NavigationSettings(private val carContext: CarContext) : Screen(carContext
tollWayToggleState = !tollWayToggleState tollWayToggleState = !tollWayToggleState
}.setChecked(tollWayToggleState).build() }.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle)) listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
listBuilder.addItem(
buildRowForScreenTemplate(
RoutingSettings(carContext, viewModel),
R.string.routing_engine
)
)
return ListTemplate.Builder() return ListTemplate.Builder()
.setSingleList(listBuilder.build()) .setSingleList(listBuilder.build())
.setHeader( .setHeader(
@@ -70,4 +76,12 @@ class NavigationSettings(private val carContext: CarContext) : Screen(carContext
.setToggle(toggle) .setToggle(toggle)
.build() .build()
} }
private fun buildRowForScreenTemplate(screen: Screen, title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setOnClickListener { screenManager.push(screen) }
.setBrowsable(true)
.build()
}
} }

View File

@@ -26,9 +26,10 @@ class RequestPermissionScreen(
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val permissions: MutableList<String?> = ArrayList() val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION) permissions.add(permission.ACCESS_FINE_LOCATION)
permissions.add("com.google.android.gms.permission.CAR_SPEED")
//permissions.add(permission.READ_CONTACTS) //permissions.add(permission.READ_CONTACTS)
val message = "This app needs access to location in order to show the map around you" val message = "This app needs access to location and to car speed"
val listener: OnClickListener = ParkedOnlyOnClickListener.create { val listener: OnClickListener = ParkedOnlyOnClickListener.create {
carContext.requestPermissions( carContext.requestPermissions(

View File

@@ -80,7 +80,6 @@ class RoutePreviewScreen(
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.route_preview)) .setTitle(carContext.getString(R.string.route_preview))
//.addEndHeaderAction(navigateAction)
.addEndHeaderAction( .addEndHeaderAction(
favoriteAction() favoriteAction()
) )

View File

@@ -0,0 +1,81 @@
package com.kouros.navigation.car.screen
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import com.kouros.data.R
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
class RoutingSettings(private val carContext: CarContext, private var viewModel: ViewModel) : Screen(carContext) {
private var routingEngine = RouteEngine.VALHALLA.ordinal
init {
routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
}
override fun onGetTemplate(): Template {
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
.addItem(
buildRowForTemplate(
R.string.valhalla,
)
)
.addItem(
buildRowForTemplate(
R.string.osrm,
)
)
.setOnSelectedListener { index: Int ->
this.onSelected(index)
}
.setSelectedIndex(routingEngine)
.build()
return templateBuilder
.addSectionedList(SectionedItemList.create(
radioList,
carContext.getString(R.string.routing_engine)
))
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.routing_engine))
.setStartHeaderAction(Action.BACK)
.build()
)
.build()
}
private fun onSelected(index: Int) {
setIntKeyValue(carContext, index, ROUTING_ENGINE)
viewModel.routingEngine.value = index
CarToast.makeText(
carContext,
(carContext
.getString(R.string.routing_engine)
+ ":"
+ " " + index), CarToast.LENGTH_LONG
)
.show()
}
private fun buildRowForTemplate(title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.build()
}
}

View File

@@ -24,10 +24,12 @@ import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.model.ViewModel
/** A screen demonstrating selectable lists. */ /** A screen demonstrating selectable lists. */
class SettingsScreen( class SettingsScreen(
carContext: CarContext, carContext: CarContext,
private var viewModel: ViewModel,
) : Screen(carContext) { ) : Screen(carContext) {
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -40,7 +42,7 @@ class SettingsScreen(
) )
listBuilder.addItem( listBuilder.addItem(
buildRowForTemplate( buildRowForTemplate(
NavigationSettings(carContext), NavigationSettings(carContext, viewModel),
R.string.navigation_settings R.string.navigation_settings
) )
) )

View File

@@ -6,6 +6,7 @@ import com.kouros.navigation.data.Constants.home2Location
import com.kouros.navigation.data.Constants.homeLocation import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import org.junit.Test import org.junit.Test
@@ -16,7 +17,7 @@ import org.junit.Test
*/ */
class ViewModelTest { class ViewModelTest {
val repo = NavigationRepository() val repo = ValhallaRepository()
val viewModel = ViewModel(repo) val viewModel = ViewModel(repo)
val model = RouteModel() val model = RouteModel()
@@ -33,6 +34,6 @@ class ViewModelTest {
toLocation.longitude = home2Location.longitude toLocation.longitude = home2Location.longitude
val route = repo.getRoute(fromLocation, toLocation, SearchFilter()) val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
model.startNavigation(route) //model.startNavigation(route)
} }
} }

View File

@@ -19,6 +19,7 @@ package com.kouros.navigation.data
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.net.Uri import android.net.Uri
import com.kouros.navigation.data.route.Lane
import io.objectbox.annotation.Entity import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id import io.objectbox.annotation.Id
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -63,7 +64,9 @@ data class StepData (
var arrivalTime : Long, var arrivalTime : Long,
var leftDistance: Double var leftDistance: Double,
var lane: List<Lane> = listOf(Lane(valid = false, indications = emptyList())),
) )
@@ -171,6 +174,8 @@ object Constants {
const val AVOID_TOLLWAY = "AvoidTollway" const val AVOID_TOLLWAY = "AvoidTollway"
const val ROUTING_ENGINE = "RoutingEngine"
const val NEXT_STEP_THRESHOLD = 100.0 const val NEXT_STEP_THRESHOLD = 100.0
const val MAXIMAL_SNAP_CORRECTION = 50.0 const val MAXIMAL_SNAP_CORRECTION = 50.0
@@ -179,7 +184,6 @@ object Constants {
const val DESTINATION_ARRIVAL_DISTANCE = 40.0 const val DESTINATION_ARRIVAL_DISTANCE = 40.0
val ROUTE_ENGINE = RouteEngine.VALHALLA.name
} }

View File

@@ -30,8 +30,6 @@ import kotlinx.serialization.json.Json
abstract class NavigationRepository { abstract class NavigationRepository {
private val placesUrl = "https://kouros-online.de/maps/placespwd";
private val nominatimUrl = "https://nominatim.openstreetmap.org/" private val nominatimUrl = "https://nominatim.openstreetmap.org/"
@@ -58,27 +56,6 @@ abstract class NavigationRepository {
return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true&countrycodes=de", false) return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true&countrycodes=de", false)
} }
fun getPlaces(): List<Place> {
val places: MutableList<Place> = ArrayList()
val placesStr = fetchUrl(placesUrl, true)
val jArray = JSONArray(placesStr)
for (i in 0..<jArray.length()) {
val json = jArray.getJSONObject(i)
val place = Place(
json.getString("id").toLong(),
json.getString("name"),
category = json.getString("category"),
latitude = json.getDouble("latitude"),
longitude = json.getDouble("longitude"),
postalCode = json.getString("postalCode"),
city = json.getString("city"),
street = json.getString("street"),
)
places.add(place)
}
return places
}
fun fetchUrl(url: String, authenticator : Boolean): String { fun fetchUrl(url: String, authenticator : Boolean): String {
try { try {
if (authenticator) { if (authenticator) {

View File

@@ -2,7 +2,6 @@ package com.kouros.navigation.data
import android.location.Location import android.location.Location
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.osrm.OsrmResponse import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute import com.kouros.navigation.data.osrm.OsrmRoute
import com.kouros.navigation.data.route.Leg import com.kouros.navigation.data.route.Leg
@@ -11,8 +10,6 @@ import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.data.valhalla.ValhallaResponse import com.kouros.navigation.data.valhalla.ValhallaResponse
import com.kouros.navigation.data.valhalla.ValhallaRoute import com.kouros.navigation.data.valhalla.ValhallaRoute
import com.kouros.navigation.utils.GeoUtils.createCenterLocation import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement

View File

@@ -5,6 +5,7 @@ import com.google.gson.annotations.SerializedName
data class Intersections( data class Intersections(
@SerializedName("in") var inV: Int? = null,
@SerializedName("out") var out: Int? = null, @SerializedName("out") var out: Int? = null,
@SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(), @SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(),
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(), @SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),

View File

@@ -1,6 +1,8 @@
package com.kouros.navigation.data.osrm package com.kouros.navigation.data.osrm
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver as RouteManeuver import com.kouros.navigation.data.route.Maneuver as RouteManeuver
import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Step
@@ -13,11 +15,12 @@ class OsrmRoute {
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) { fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) {
val waypoints = mutableListOf<List<Double>>() val waypoints = mutableListOf<List<Double>>()
val summary = Summary() val summary = Summary()
summary.distance = routeJson.routes.first().distance!! summary.distance = routeJson.routes.first().distance!! / 1000
summary.duration = routeJson.routes.first().duration!! summary.duration = routeJson.routes.first().duration!! / 1000
val steps = mutableListOf<Step>() val steps = mutableListOf<Step>()
var stepIndex = 0 var stepIndex = 0
routeJson.routes.first().legs.first().steps.forEach { routeJson.routes.first().legs.first().steps.forEach {
val intersections = mutableListOf<Intersection>()
if (it.maneuver != null) { if (it.maneuver != null) {
val points = decodePolyline(it.geometry!!, 5) val points = decodePolyline(it.geometry!!, 5)
waypoints.addAll(points) waypoints.addAll(points)
@@ -27,7 +30,17 @@ class OsrmRoute {
type = convertType(it.maneuver!!), type = convertType(it.maneuver!!),
waypoints = points waypoints = points
) )
val step = Step( index = stepIndex, name = it.name!!, distance = it.distance!!, duration = it.duration!!, maneuver = maneuver) it.intersections.forEach { it2 ->
if (it2.location[0] != 0.0) {
val lanes = mutableListOf<Lane>()
it2.lanes.forEach { it3 ->
val lane = Lane(it3.valid, it3.indications)
lanes.add(lane)
}
intersections.add(Intersection(it2.location, lanes))
}
}
val step = Step( index = stepIndex, name = it.name!!, distance = it.distance!! / 1000, duration = it.duration!!, maneuver = maneuver, intersection = intersections)
steps.add(step) steps.add(step)
stepIndex += 1 stepIndex += 1
} }
@@ -53,12 +66,34 @@ class OsrmRoute {
ManeuverType.continue_.value -> { ManeuverType.continue_.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
} }
ManeuverType.turn.value -> { ManeuverType.turn.value,
ManeuverType.endOfRoad.value -> {
if (maneuver.modifier == "right") { if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
} }
} }
ManeuverType.turn.value,
ManeuverType.endOfRoad.value,
ManeuverType.onRamp.value
-> {
if (maneuver.modifier == "left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
} }
}
ManeuverType.fork.value
-> {
if (maneuver.modifier == "slight left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
}
}
ManeuverType.fork.value
-> {
if (maneuver.modifier == "slight right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
}
}
return newType return newType
} }
} }

View File

@@ -64,6 +64,7 @@ class Overpass {
} }
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String) : List<Elements> { fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String) : List<Elements> {
try {
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream) val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
outputStreamWriter.write(searchQuery) outputStreamWriter.write(searchQuery)
outputStreamWriter.flush() outputStreamWriter.flush()
@@ -74,9 +75,10 @@ class Overpass {
.use { it.readText() } // defaults to UTF-8 .use { it.readText() } // defaults to UTF-8
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
val overpass = gson.fromJson(response, Amenity::class.java) val overpass = gson.fromJson(response, Amenity::class.java)
// println("Overpass: $response")
return overpass.elements return overpass.elements
} }
} catch (e: Exception) {
}
return emptyList() return emptyList()
} }
} }

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.route
import java.util.Collections
data class Intersection(
val location: ArrayList<Double> = arrayListOf(0.0, 0.0),
val lane : List<Lane> = Collections.emptyList<Lane>(),
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.route
data class Lane (
val valid: Boolean,
var indications: List<String>,
)

View File

@@ -1,10 +1,15 @@
package com.kouros.navigation.data.route package com.kouros.navigation.data.route
class Step( import android.location.Location
import com.kouros.navigation.utils.location
data class Step(
var index : Int = 0, var index : Int = 0,
var waypointIndex : Int = 0, var waypointIndex : Int = 0,
var wayPointLocation : Location = location(0.0,0.0),
val maneuver: Maneuver, val maneuver: Maneuver,
val duration: Double = 0.0, val duration: Double = 0.0,
val distance: Double = 0.0, val distance: Double = 0.0,
val name : String = "", val name : String = "",
val intersection: List<Intersection> = mutableListOf(),
) )

View File

@@ -1,8 +1,10 @@
package com.kouros.navigation.data.valhalla package com.kouros.navigation.data.valhalla
import androidx.car.app.navigation.model.Maneuver
import com.kouros.data.R
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.route.Leg import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver import com.kouros.navigation.data.route.Maneuver as RouteManeuver
import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
@@ -18,10 +20,11 @@ class ValhallaRoute {
val steps = mutableListOf<Step>() val steps = mutableListOf<Step>()
var stepIndex = 0 var stepIndex = 0
routeJson.legs[0].maneuvers.forEach { routeJson.legs[0].maneuvers.forEach {
val maneuver = Maneuver( val maneuver = RouteManeuver(
bearingBefore = 0, bearingBefore = 0,
bearingAfter = it.bearingAfter, bearingAfter = it.bearingAfter,
type = it.type, //type = it.type,
type = convertType(it),
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1) waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1)
) )
var name = "" var name = ""
@@ -40,4 +43,56 @@ class ValhallaRoute {
.legs(listOf(leg)) .legs(listOf(leg))
.waypoints(waypoints) .waypoints(waypoints)
} }
fun convertType(maneuver: Maneuvers): Int {
var newType = 0
when (maneuver.type) {
ManeuverType.None.value -> {
newType = Maneuver.TYPE_STRAIGHT
}
ManeuverType.Destination.value,
ManeuverType.DestinationRight.value,
ManeuverType.DestinationLeft.value,
-> {
newType = Maneuver.TYPE_DESTINATION
}
ManeuverType.Right.value -> {
newType = Maneuver.TYPE_TURN_NORMAL_RIGHT
}
ManeuverType.Left.value -> {
newType = Maneuver.TYPE_TURN_NORMAL_LEFT
}
ManeuverType.RampRight.value -> {
newType = Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT
}
ManeuverType.RampLeft.value -> {
newType = Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT
}
ManeuverType.ExitRight.value -> {
newType = Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
ManeuverType.StayRight.value -> {
newType = Maneuver.TYPE_KEEP_RIGHT
}
ManeuverType.StayLeft.value -> {
newType = Maneuver.TYPE_KEEP_LEFT
}
ManeuverType.RoundaboutEnter.value -> {
newType = Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
}
ManeuverType.RoundaboutExit.value -> {
newType = Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
}
}
return newType
}
} }

View File

@@ -0,0 +1,33 @@
package com.kouros.navigation.model
import android.content.Context
import android.content.res.Configuration
import androidx.compose.foundation.isSystemInDarkTheme
import com.kouros.data.R
import org.maplibre.compose.style.BaseStyle
class BaseStyleModel {
fun isDarkTheme(context: Context): Boolean {
return context.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
fun readStyle(context: Context, darkModeSettings: Int, isCarDarkMode: Boolean): BaseStyle.Json {
println("BaseStyle ${isDarkTheme(context)}")
val liberty = when(darkModeSettings) {
0 -> context.resources.openRawResource(R.raw.liberty)
1 -> context.resources.openRawResource(R.raw.liberty_night)
else -> {
if (isDarkTheme(context) || isCarDarkMode) {
context.resources.openRawResource(R.raw.liberty_night)
} else {
context.resources.openRawResource(R.raw.liberty)
}
}
}
val libertyString = liberty.bufferedReader().use { it.readText() }
val baseStyle = BaseStyle.Json(libertyString)
return baseStyle
}
}

View File

@@ -1,26 +1,31 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Matrix
import android.location.Location import android.location.Location
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Constants.ROUTE_ENGINE import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.valhalla.ManeuverType
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.valhalla.ManeuverType
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -37,6 +42,9 @@ open class RouteModel() {
val lastSpeedLocation: Location = location(0.0, 0.0), val lastSpeedLocation: Location = location(0.0, 0.0),
val lastSpeedIndex: Int = 0, val lastSpeedIndex: Int = 0,
val maxSpeed: Int = 0, val maxSpeed: Int = 0,
val location: Location = location(0.0, 0.0),
val lastLocation: Location = location(0.0, 0.0),
val bearing : Float = 0F
) )
var routeState = RouteState() var routeState = RouteState()
@@ -52,11 +60,14 @@ open class RouteModel() {
get() = routeState.route!!.legs!!.first() get() = routeState.route!!.legs!!.first()
fun startNavigation(routeString: String, context: Context) { fun startNavigation(routeString: String, context: Context) {
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE) val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
val newRoute = Route.Builder() var newRoute = Route.Builder()
.routeEngine(routeEngine) .routeEngine(routeEngine)
.route(routeString) .route(routeString)
.build() .build()
// TODO:
newRoute = newRoute.copy(centerLocation = createCenterLocation(newRoute.routeGeoJson))
println("Route ${newRoute.centerLocation}")
this.routeState = routeState.copy( this.routeState = routeState.copy(
route = newRoute, route = newRoute,
isNavigating = true isNavigating = true
@@ -74,8 +85,10 @@ open class RouteModel() {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun updateLocation(location: Location, viewModel: ViewModel) { fun updateLocation(location: Location, viewModel: ViewModel) {
routeState = routeState.copy(location = location)
findStep(location) findStep(location)
updateSpeedLimit(location, viewModel) updateSpeedLimit(location, viewModel)
} }
private fun findStep(location: Location) { private fun findStep(location: Location) {
@@ -89,6 +102,9 @@ open class RouteModel() {
nearestDistance = distance nearestDistance = distance
route.currentStep = step.index route.currentStep = step.index
step.waypointIndex = wayIndex step.waypointIndex = wayIndex
step.wayPointLocation = location(waypoint[0], waypoint[1])
val bearing = routeState.lastLocation.bearingTo(location)
this.routeState = routeState.copy(lastLocation = location, bearing = bearing)
} }
} }
if (nearestDistance == 0F) { if (nearestDistance == 0F) {
@@ -100,7 +116,19 @@ open class RouteModel() {
break break
} }
} }
//println("Current Index ${route.currentStep} WayPoint: ${route.currentStep().waypointIndex}") }
private fun currentIntersection(location: Location): Intersection {
var inter = Intersection()
var nearestDistance = 100000.0f
route.currentStep().intersection.forEach {
val distance = location.distanceTo(location(it.location[0], it.location[1]))
if (distance < nearestDistance) {
nearestDistance = distance
inter = it
}
}
return inter
} }
fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking { fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking {
@@ -133,29 +161,31 @@ open class RouteModel() {
var maneuverType = if (hasArrived(currentStep.maneuver.type)) { var maneuverType = if (hasArrived(currentStep.maneuver.type)) {
currentStep.maneuver.type currentStep.maneuver.type
} else { } else {
ManeuverType.None.value Maneuver.TYPE_STRAIGHT
} }
// Get the single, correct maneuver for this state // Get the single, correct maneuver for this state
val relevantManeuver = if (shouldAdvance) { val relevantStep = if (shouldAdvance) {
route.nextStep() // This advances the route's state route.nextStep() // This advances the route's state
} else { } else {
route.currentStep() route.currentStep()
} }
// Safely get the street name from the maneuver // Safely get the street name from the maneuver
val streetName = relevantManeuver.name val streetName = relevantStep.name
if (shouldAdvance) { if (shouldAdvance) {
maneuverType = relevantManeuver.maneuver.type maneuverType = relevantStep.maneuver.type
} }
val maneuverIconPair = maneuverIcon(maneuverType) val maneuverIconPair = maneuverIcon(maneuverType)
routeState = routeState.copy(maneuverType = maneuverIconPair.first) routeState = routeState.copy(maneuverType = maneuverIconPair.first)
// Construct and return the final StepData object // Construct and return the final StepData object
val intersection = currentIntersection(routeState.location)
return StepData( return StepData(
streetName, streetName,
distanceToNextStep, distanceToNextStep,
maneuverIconPair.first, maneuverIconPair.first,
maneuverIconPair.second, maneuverIconPair.second,
arrivalTime(), arrivalTime(),
travelLeftDistance() travelLeftDistance(),
intersection.lane
) )
} }
@@ -168,6 +198,7 @@ open class RouteModel() {
when (distanceLeft) { when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> { in 0.0..NEXT_STEP_THRESHOLD -> {
} }
else -> { else -> {
if (step.name.isNotEmpty()) { if (step.name.isNotEmpty()) {
text = step.name text = step.name
@@ -243,68 +274,49 @@ open class RouteModel() {
} }
fun maneuverIcon(routeManeuverType: Int): (Pair<Int, Int>) { fun maneuverIcon(routeManeuverType: Int): (Pair<Int, Int>) {
var type = Maneuver.TYPE_DEPART
var currentTurnIcon = R.drawable.ic_turn_name_change var currentTurnIcon = R.drawable.ic_turn_name_change
when (routeManeuverType) { when (routeManeuverType) {
ManeuverType.None.value -> { Maneuver.TYPE_STRAIGHT -> {
type = Maneuver.TYPE_STRAIGHT
currentTurnIcon = R.drawable.ic_turn_name_change currentTurnIcon = R.drawable.ic_turn_name_change
} }
ManeuverType.Destination.value, Maneuver.TYPE_DESTINATION,
ManeuverType.DestinationRight.value,
ManeuverType.DestinationLeft.value,
-> { -> {
type = Maneuver.TYPE_DESTINATION
currentTurnIcon = R.drawable.ic_turn_destination currentTurnIcon = R.drawable.ic_turn_destination
} }
ManeuverType.Right.value -> { Maneuver.TYPE_TURN_NORMAL_RIGHT -> {
type = Maneuver.TYPE_TURN_NORMAL_RIGHT
currentTurnIcon = R.drawable.ic_turn_normal_right currentTurnIcon = R.drawable.ic_turn_normal_right
} }
ManeuverType.Left.value -> { Maneuver.TYPE_TURN_NORMAL_LEFT -> {
type = Maneuver.TYPE_TURN_NORMAL_LEFT
currentTurnIcon = R.drawable.ic_turn_normal_left currentTurnIcon = R.drawable.ic_turn_normal_left
} }
ManeuverType.RampRight.value -> { Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT -> {
type = Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT
currentTurnIcon = R.drawable.ic_turn_slight_right currentTurnIcon = R.drawable.ic_turn_slight_right
} }
ManeuverType.RampLeft.value -> { Maneuver.TYPE_TURN_SLIGHT_RIGHT -> {
type = Maneuver.TYPE_TURN_NORMAL_LEFT
currentTurnIcon = R.drawable.ic_turn_normal_left
}
ManeuverType.ExitRight.value -> {
type = Maneuver.TYPE_TURN_SLIGHT_RIGHT
currentTurnIcon = R.drawable.ic_turn_slight_right currentTurnIcon = R.drawable.ic_turn_slight_right
} }
ManeuverType.StayRight.value -> { Maneuver.TYPE_KEEP_RIGHT -> {
type = Maneuver.TYPE_KEEP_RIGHT
currentTurnIcon = R.drawable.ic_turn_name_change currentTurnIcon = R.drawable.ic_turn_name_change
} }
Maneuver.TYPE_KEEP_LEFT -> {
ManeuverType.StayLeft.value -> {
type = Maneuver.TYPE_KEEP_LEFT
currentTurnIcon = R.drawable.ic_turn_name_change currentTurnIcon = R.drawable.ic_turn_name_change
} }
Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> {
ManeuverType.RoundaboutEnter.value -> {
type = Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
currentTurnIcon = R.drawable.ic_roundabout_ccw currentTurnIcon = R.drawable.ic_roundabout_ccw
} }
ManeuverType.RoundaboutExit.value -> { Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> {
type = Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
currentTurnIcon = R.drawable.ic_roundabout_ccw currentTurnIcon = R.drawable.ic_roundabout_ccw
} }
} }
return Pair(type, currentTurnIcon) return Pair(routeManeuverType, currentTurnIcon)
} }
fun isNavigating(): Boolean { fun isNavigating(): Boolean {
@@ -317,4 +329,79 @@ open class RouteModel() {
|| type == ManeuverType.Destination.value || type == ManeuverType.Destination.value
|| type == ManeuverType.DestinationLeft.value || type == ManeuverType.DestinationLeft.value
} }
fun createLaneIcon(context: Context, stepData: StepData): IconCompat {
val bitmaps = mutableListOf<Bitmap>()
stepData.lane.forEach {
if (it.indications.isNotEmpty()) {
it.indications.forEach { it2 ->
val resource = laneToResource(it2, it, stepData)
if (it2 != "none") {
println("Direction $resource")
if (resource.isNotEmpty()) {
val id = resourceId( resource);
val bitMap = BitmapFactory.decodeResource(context.resources, id)
bitmaps.add(bitMap)
}
}
}
}
}
return IconCompat.createWithBitmap(overlay(bitmaps = bitmaps))
}
fun overlay(bitmaps: List<Bitmap>): Bitmap {
val matrix = Matrix()
if (bitmaps.size == 1) {
return bitmaps.first()
}
val bmOverlay = createBitmap(
bitmaps.first().getWidth() * bitmaps.size,
bitmaps.first().getHeight(),
bitmaps.first().getConfig()!!
)
val canvas = Canvas(bmOverlay)
canvas.drawBitmap(bitmaps.first(), matrix, null)
var i = 0
bitmaps.forEach {
if (i > 0) {
matrix.setTranslate(i * 40F, 0F)
canvas.drawBitmap(it, matrix, null)
}
i++
}
return bmOverlay
}
private fun laneToResource(direction: String, lane: Lane, stepData: StepData): String {
println("Maneuver ${stepData.maneuverType}")
return when (val direction = direction.replace(" ", "_")) {
"left" -> if (stepData.maneuverType == Maneuver.TYPE_TURN_NORMAL_LEFT) "${direction}_valid" else "${direction}_not_valid"
"right" -> if (stepData.maneuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_valid" else "${direction}_not_valid"
"straight" -> if (stepData.maneuverType == Maneuver.TYPE_STRAIGHT) "${direction}_valid" else "${direction}_not_valid"
"slight_right" -> if (stepData.maneuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT) "${direction}_valid" else "${direction}_not_valid"
"slight_left" -> if (stepData.maneuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT) "${direction}_valid" else "${direction}_not_valid"
else -> {""}
}
}
fun resourceId(
variableName: String,
): Int {
return when(variableName) {
"left_not_valid" -> R.drawable.left_not_valid
"left_valid" -> R.drawable.left_valid
"left_valid_right_not_valid" -> R.drawable.left_valid_right_not_valid
"right_not_valid" -> R.drawable.right_not_valid
"right_valid" -> R.drawable.right_valid
"slight_right_not_valid" -> R.drawable.slight_right_not_valid
"slight_right_valid" -> R.drawable.slight_right_valid
"straight_not_valid" -> R.drawable.straight_not_valid
"straight_not_valid_right_valid" -> R.drawable.straight_not_valid_right_valid
"straight_valid" -> R.drawable.straight_valid
else -> {R.drawable.ic_close_white_24dp}
}
}
} }

View File

@@ -67,7 +67,12 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData() MutableLiveData()
} }
fun loadRecentPlace(location: Location) { val routingEngine: MutableLiveData<Int> by lazy {
MutableLiveData()
}
fun loadRecentPlace(location: Location, context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
@@ -79,12 +84,12 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
query.close() query.close()
for (place in results) { for (place in results) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
//val distance = repository.getRouteDistance(location, plLocation, SearchFilter()) val distance = repository.getRouteDistance(location, plLocation, SearchFilter(), context)
//place.distance = distance.toFloat() place.distance = distance.toFloat()
//if (place.distance == 0F) { if (place.distance > 1F) {
recentPlace.postValue(place) recentPlace.postValue(place)
return@launch return@launch
//} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()

View File

@@ -4,7 +4,7 @@ import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import androidx.core.content.edit import androidx.core.content.edit
import com.kouros.navigation.data.Constants.ROUTE_ENGINE import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
@@ -26,7 +26,7 @@ import kotlin.time.Duration.Companion.seconds
object NavigationUtils { object NavigationUtils {
fun getRouteEngine(context: Context): ViewModel { fun getRouteEngine(context: Context): ViewModel {
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE) val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
return when (routeEngine) { return when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository()) RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
else -> ViewModel(OsrmRepository()) else -> ViewModel(OsrmRepository())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M701,600L614,550L754,446L840,496L701,600ZM160,800L160,720L360,720Q360,720 360,720Q360,720 360,720L360,482L240,413Q211,396 202.5,364.5Q194,333 211,304L271,200Q288,171 319.5,162.5Q351,154 380,171L761,391L517,573L440,529L440,720Q440,753 416.5,776.5Q393,800 360,800L160,800Z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

View File

@@ -30,4 +30,7 @@
<string name="reject_action_title">Reject</string> <string name="reject_action_title">Reject</string>
<string name="ok_action_title">OK</string> <string name="ok_action_title">OK</string>
<string name="search_action_title">Search</string> <string name="search_action_title">Search</string>
<string name="valhalla">Valhalla</string>
<string name="osrm">Osrm</string>
<string name="routing_engine">Routing engine</string>
</resources> </resources>

View File

@@ -13,7 +13,7 @@ junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
kotlinxSerializationJson = "1.9.0" kotlinxSerializationJson = "1.9.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
composeBom = "2025.12.00" composeBom = "2025.12.01"
appcompat = "1.7.1" appcompat = "1.7.1"
material = "1.13.0" material = "1.13.0"
carApp = "1.7.0" carApp = "1.7.0"
@@ -42,6 +42,7 @@ foundationLayout = "1.10.0"
[libraries] [libraries]
android-sdk-turf = { module = "org.maplibre.gl:android-sdk-turf", version.ref = "androidSdkTurf" } android-sdk-turf = { module = "org.maplibre.gl:android-sdk-turf", version.ref = "androidSdkTurf" }
androidx-app-projected = { module = "androidx.car.app:app-projected" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }