Before TomTom Routing

This commit is contained in:
Dimitris
2026-01-29 12:13:37 +01:00
parent 7db7cba4fb
commit eac5b56bcb
51 changed files with 5825 additions and 212 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 = 28 versionCode = 32
versionName = "0.1.3.28" versionName = "0.1.3.32"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -2,6 +2,7 @@ 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.tomtom.TomTomRepository
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.BaseStyleModel
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
@@ -14,4 +15,5 @@ val appModule = module {
viewModelOf(::ViewModel) viewModelOf(::ViewModel)
singleOf(::ValhallaRepository) singleOf(::ValhallaRepository)
singleOf(::OsrmRepository) singleOf(::OsrmRepository)
singleOf(::TomTomRepository)
} }

View File

@@ -78,6 +78,8 @@ class MainActivity : ComponentActivity() {
val routeModel = RouteModel() val routeModel = RouteModel()
var tilt = 50.0 var tilt = 50.0
val useMock = false val useMock = false
var currentIndex = 0
val stepData: MutableLiveData<StepData> by lazy { val stepData: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>() MutableLiveData<StepData>()
} }
@@ -113,7 +115,7 @@ class MainActivity : ComponentActivity() {
lateinit var baseStyle: BaseStyle.Json lateinit var baseStyle: BaseStyle.Json
init { init {
navigationViewModel.route.observe(this, observer)
} }
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]) @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
@@ -127,13 +129,14 @@ class MainActivity : ComponentActivity() {
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 { _: android.location.Location? ->
if (useMock) { if (useMock) {
mock = MockLocation(locationManager) mock = MockLocation(locationManager)
mock.setMockLocation( mock.setMockLocation(
homeHohenwaldeck.latitude, homeVogelhart.latitude,
homeHohenwaldeck.longitude homeVogelhart.longitude
) )
navigationViewModel.route.observe(this, observer)
} }
} }
enableEdgeToEdge() enableEdgeToEdge()
@@ -156,7 +159,12 @@ class MainActivity : ComponentActivity() {
Content() Content()
// auto navigate // auto navigate
if (useMock) { if (useMock) {
navigationViewModel.loadRoute(applicationContext, homeHohenwaldeck, homeVogelhart, 0F) navigationViewModel.loadRoute(
applicationContext,
homeVogelhart,
homeHohenwaldeck,
0F
)
} }
}, },
) )
@@ -249,6 +257,9 @@ 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)
// if (currentIndex == 0)
// navigationViewModel.loadTraffic(applicationContext, currentLocation, 0f)
// currentIndex = 1
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing) val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
with(routeModel) { with(routeModel) {
if (isNavigating()) { if (isNavigating()) {
@@ -326,7 +337,7 @@ class MainActivity : ComponentActivity() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) { for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
var deviation = 0.0 val deviation = 0.0
if (index in 0..routeModel.curRoute.waypoints.size) { if (index in 0..routeModel.curRoute.waypoints.size) {
mock.setMockLocation(waypoint[1] + deviation, waypoint[0]) mock.setMockLocation(waypoint[1] + deviation, waypoint[0])
delay(500L) // delay(500L) //
@@ -362,6 +373,7 @@ class MainActivity : ComponentActivity() {
mock.setMockLocation(48.119357, 11.599130) mock.setMockLocation(48.119357, 11.599130)
} }
} }
fun gpx(context: Context) { fun gpx(context: Context) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val parser = GPXParser() val parser = GPXParser()

View File

@@ -16,6 +16,7 @@ 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.car.map.rememberBaseStyle import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.tomtom.TrafficData
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.LocationTrackingEffect
@@ -65,6 +66,7 @@ fun MapView(
cameraState, cameraState,
rememberBaseStyle, rememberBaseStyle,
route, route,
emptyMap(),
ViewStyle.VIEW ViewStyle.VIEW
) )
LocationTrackingEffect( LocationTrackingEffect(

View File

@@ -28,6 +28,10 @@ fun NavigationInfo(step: StepData?, nextStep: StepData?) {
contentDescription = stringResource(id = R.string.accept_action_title), contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp), modifier = Modifier.size(48.dp, 48.dp),
) )
if (step.currentManeuverType == 46
|| step.currentManeuverType == 45) {
Text(text ="Exit ${step.exitNumber}", fontSize = 20.sp)
}
Column { Column {
if (step.leftStepDistance < 1000) { if (step.leftStepDistance < 1000) {
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp) Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp)

View File

@@ -57,6 +57,15 @@ fun NavigationSheet(
modifier = Modifier.size(24.dp, 24.dp), modifier = Modifier.size(24.dp, 24.dp),
) )
} }
Button(onClick = {
simulateNavigation()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_zoom_in_24),
"Stop",
modifier = Modifier.size(24.dp, 24.dp),
)
}
} }
Spacer(Modifier.size(30.dp)) Spacer(Modifier.size(30.dp))
if (!routeModel.isNavigating()) { if (!routeModel.isNavigating()) {

View File

@@ -30,21 +30,18 @@ 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.CAR_LOCATION import com.kouros.navigation.data.Constants.CAR_LOCATION
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.TAG import com.kouros.navigation.data.Constants.TAG
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
import com.kouros.navigation.data.tomtom.TomTomRepository
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.GeoUtils.snapLocation
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.getViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel
import org.maplibre.compose.style.BaseStyle
class NavigationSession : Session(), NavigationScreen.Listener { class NavigationSession : Session(), NavigationScreen.Listener {
@@ -93,8 +90,6 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lateinit var navigationViewModel: ViewModel lateinit var navigationViewModel: ViewModel
lateinit var baseStyle: BaseStyle.Json
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> = val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
OnCarDataAvailableListener { data -> OnCarDataAvailableListener { data ->
if (data.location.status == CarValue.STATUS_SUCCESS) { if (data.location.status == CarValue.STATUS_SUCCESS) {
@@ -129,21 +124,21 @@ class NavigationSession : Session(), NavigationScreen.Listener {
fun onRoutingEngineStateUpdated(routeEngine : Int) { fun onRoutingEngineStateUpdated(routeEngine : Int) {
navigationViewModel = when (routeEngine) { navigationViewModel = when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository()) RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
else -> ViewModel(OsrmRepository()) RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
else -> ViewModel(TomTomRepository())
} }
} }
override fun onCreateScreen(intent: Intent): Screen { override fun onCreateScreen(intent: Intent): Screen {
navigationViewModel = getViewModel(carContext) navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated) navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
routeModel = RouteCarModel() routeModel = RouteCarModel()
val darkMode = getIntKeyValue(carContext, Constants.DARK_MODE_SETTINGS) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)
baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, baseStyle)
navigationScreen = navigationScreen =
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel) NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)

View File

@@ -29,10 +29,13 @@ 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.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.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.homeVogelhart import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
@@ -49,8 +52,7 @@ 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)
@@ -70,14 +72,20 @@ class SurfaceRenderer(
var height = 0 var height = 0
var lastBearing = 0.0 var lastBearing = 0.0
val routeData = MutableLiveData("") val routeData = MutableLiveData("")
val trafficData = MutableLiveData(emptyMap<String, String>())
val speedCamerasData = MutableLiveData("") val speedCamerasData = MutableLiveData("")
val speed = MutableLiveData(0F) val speed = MutableLiveData(0F)
lateinit var centerLocation: Location
var viewStyle = ViewStyle.VIEW var viewStyle = ViewStyle.VIEW
lateinit var centerLocation: Location
var previewDistance = 0.0 var previewDistance = 0.0
lateinit var mapView: ComposeView lateinit var mapView: ComposeView
var tilt = 55.0 var tilt = 55.0
var countDownTimerActive = false
val style: MutableLiveData<BaseStyle> by lazy {
MutableLiveData()
}
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback { val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
lateinit var lifecycleOwner: CustomLifecycleOwner lateinit var lifecycleOwner: CustomLifecycleOwner
@@ -160,6 +168,7 @@ class SurfaceRenderer(
init { init {
lifecycle.addObserver(this) lifecycle.addObserver(this)
speed.value = 0F speed.value = 0F
} }
fun onConnectionStateUpdated(connectionState: Int) { fun onConnectionStateUpdated(connectionState: Int) {
@@ -170,16 +179,24 @@ class SurfaceRenderer(
} }
} }
fun onBaseStyleStateUpdated(style: BaseStyle) {
}
@Composable @Composable
fun MapView() { fun MapView() {
//println("DarkMode ${carContext.isDarkMode}")
val darkMode = getIntKeyValue(carContext, Constants.DARK_MODE_SETTINGS)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
val position: CameraPosition? by cameraPosition.observeAsState() val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState() val route: String? by routeData.observeAsState()
val traffic: Map<String, String> ? by trafficData.observeAsState()
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 rememberBaseStyle = rememberBaseStyle(baseStyle)
MapLibre(carContext, cameraState, rememberBaseStyle, route, viewStyle, speedCameras) MapLibre(carContext, cameraState, rememberBaseStyle, route, traffic, viewStyle, speedCameras)
ShowPosition(cameraState, position, paddingValues) ShowPosition(cameraState, position, paddingValues)
} }
@@ -217,6 +234,7 @@ class SurfaceRenderer(
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
CarConnection(carContext).type.observe(owner, ::onConnectionStateUpdated) CarConnection(carContext).type.observe(owner, ::onConnectionStateUpdated)
style.observe(owner, :: onBaseStyleStateUpdated)
Log.i(TAG, "SurfaceRenderer created") Log.i(TAG, "SurfaceRenderer created")
carContext.getCarService(AppManager::class.java) carContext.getCarService(AppManager::class.java)
.setSurfaceCallback(mSurfaceCallback) .setSurfaceCallback(mSurfaceCallback)
@@ -288,6 +306,10 @@ class SurfaceRenderer(
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
} }
fun setTrafficData(traffic: Map<String, String> ) {
trafficData.value = traffic as MutableMap<String, String>?
}
fun setPreviewRouteData(routeModel: RouteModel) { fun setPreviewRouteData(routeModel: RouteModel) {
viewStyle = ViewStyle.PREVIEW viewStyle = ViewStyle.PREVIEW
with(routeModel) { with(routeModel) {

View File

@@ -2,6 +2,7 @@ package com.kouros.navigation.car.map
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import androidx.car.app.connection.CarConnection
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -10,18 +11,15 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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
@@ -34,19 +32,21 @@ import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING 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.ObjectBox
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.data.tomtom.TrafficData
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
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
import org.maplibre.compose.expressions.ast.Expression
import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.expressions.dsl.exponential import org.maplibre.compose.expressions.dsl.exponential
import org.maplibre.compose.expressions.dsl.image import org.maplibre.compose.expressions.dsl.image
import org.maplibre.compose.expressions.dsl.interpolate import org.maplibre.compose.expressions.dsl.interpolate
import org.maplibre.compose.expressions.dsl.zoom import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.expressions.value.ColorValue
import org.maplibre.compose.layers.Anchor import org.maplibre.compose.layers.Anchor
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
@@ -92,10 +92,10 @@ fun MapLibre(
cameraState: CameraState, cameraState: CameraState,
baseStyle: BaseStyle.Json, baseStyle: BaseStyle.Json,
route: String?, route: String?,
traffic: Map<String, String> ?,
viewStyle: ViewStyle, viewStyle: ViewStyle,
speedCameras: String? = "" speedCameras: String? = ""
) { ) {
MaplibreMap( MaplibreMap(
options = MapOptions( options = MapOptions(
ornamentOptions = ornamentOptions =
@@ -111,7 +111,7 @@ fun MapLibre(
if (viewStyle == ViewStyle.AMENITY_VIEW) { if (viewStyle == ViewStyle.AMENITY_VIEW) {
AmenityLayer(route) AmenityLayer(route)
} else { } else {
RouteLayer(route) RouteLayer(route, traffic!!)
} }
SpeedCameraLayer(speedCameras) SpeedCameraLayer(speedCameras)
} }
@@ -121,7 +121,7 @@ fun MapLibre(
} }
@Composable @Composable
fun RouteLayer(routeData: String?) { fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
if (routeData != null && routeData.isNotEmpty()) { if (routeData != null && routeData.isNotEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
LineLayer( LineLayer(
@@ -153,6 +153,49 @@ fun RouteLayer(routeData: String?) {
), ),
) )
} }
trafficData.forEach {
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
LineLayer(
id = "traffic-${it.key}-casing",
source = traffic,
color = const(Color.White),
width =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4.dp),
6 to const(0.8.dp),
7 to const(2.0.dp),
20 to const(24.dp),
),
)
LineLayer(
id = "traffic-${it.key}",
source = traffic,
color = trafficColor(it.key),
width =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4.dp),
6 to const(0.7.dp),
7 to const(1.75.dp),
20 to const(22.dp),
),
)
}
}
fun trafficColor(key: String): Expression<ColorValue> {
when (key) {
"queuing" -> return const(Color(0xFFD24417))
"stationary" -> return const(Color(0xFFFF0000))
"heavy" -> return const(Color(0xFF6B0404))
"slow" -> return const(Color(0xFFC41F1F))
"roadworks" -> return const(Color(0xFF7A631A))
}
return const(Color.Blue)
} }
@Composable @Composable
@@ -270,7 +313,7 @@ private fun CurrentSpeed(
curSpeed: Float, curSpeed: Float,
maxSpeed: Int maxSpeed: Int
) { ) {
val radius = 32 val radius = 34
Box( Box(
modifier = Modifier modifier = Modifier
.padding( .padding(
@@ -336,7 +379,7 @@ private fun MaxSpeed(
height: Int, height: Int,
maxSpeed: Int, maxSpeed: Int,
) { ) {
val radius = 20 val radius = 24
Box( Box(
modifier = Modifier modifier = Modifier
.padding( .padding(

View File

@@ -16,6 +16,7 @@
package com.kouros.navigation.car.navigation package com.kouros.navigation.car.navigation
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.car.app.AppManager import androidx.car.app.AppManager
@@ -26,22 +27,25 @@ import androidx.car.app.model.Alert
import androidx.car.app.model.AlertCallback import androidx.car.app.model.AlertCallback
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.CarIconSpan
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.Lane
import androidx.car.app.navigation.model.LaneDirection 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.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
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
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import org.maplibre.compose.expressions.dsl.step
import java.util.Collections import java.util.Collections
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.text.trim
/** A class that provides models for the routing demos. */ /** A class that provides models for the routing demos. */
class RouteCarModel() : RouteModel() { class RouteCarModel() : RouteModel() {
@@ -51,12 +55,17 @@ class RouteCarModel() : RouteModel() {
val stepData = currentStep() val stepData = currentStep()
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(stepData.instruction) createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
}
val step = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
.setManeuver( .setManeuver(
Maneuver.Builder(stepData.currentManeuverType) maneuver.build()
.setIcon(createCarIcon(carContext, stepData.icon))
.build()
) )
if (destination.street != null) { if (destination.street != null) {
step.setRoad(destination.street!!) step.setRoad(destination.street!!)
@@ -72,12 +81,16 @@ class RouteCarModel() : RouteModel() {
val stepData = nextStep() val stepData = nextStep()
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(stepData.instruction) createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
}
val step = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
.setManeuver( .setManeuver(
Maneuver.Builder(stepData.currentManeuverType) maneuver.build()
.setIcon(createCarIcon(carContext, stepData.icon))
.build()
) )
.build() .build()
return step return step
@@ -109,8 +122,8 @@ class RouteCarModel() : RouteModel() {
timeToDestinationMillis timeToDestinationMillis
) )
) )
.setRemainingTimeColor(CarColor.YELLOW) .setRemainingTimeColor(CarColor.GREEN)
.setRemainingDistanceColor(CarColor.RED) .setRemainingDistanceColor(CarColor.BLUE)
if (travelMessage.isNotEmpty()) { if (travelMessage.isNotEmpty()) {
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px)) travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
@@ -203,6 +216,18 @@ class RouteCarModel() : RouteModel() {
} }
} }
private fun createStringWithIcon(
carContext: CarContext,
text: String,
@DrawableRes iconRes: Int
): SpannableString {
val start = 0
val end = text.length
val span = CarIconSpan.create(createCarIcon(carContext, iconRes), CarIconSpan.ALIGN_CENTER)
val spannableString = SpannableString(text)
spannableString.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannableString
}
fun createString( fun createString(
text: String text: String
): SpannableString { ): SpannableString {
@@ -222,7 +247,7 @@ class RouteCarModel() : RouteModel() {
return CarIcon.Builder(iconCompat).build() 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, createCarIcon(carContext, R.drawable.speed_camera_24px))) .showAlert(createAlert(carContext, distance, maxSpeed, createCarIcon(carContext, R.drawable.speed_camera_24px)))
} }

View File

@@ -31,10 +31,16 @@ import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
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.data.tomtom.Features
import com.kouros.navigation.data.tomtom.Geometry
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.location import com.kouros.navigation.utils.location
import java.time.LocalDateTime
import java.time.Period
import java.time.ZoneOffset
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.time.Duration
class NavigationScreen( class NavigationScreen(
carContext: CarContext, carContext: CarContext,
@@ -55,6 +61,7 @@ class NavigationScreen(
var recentPlace = Place() var recentPlace = Place()
var navigationType = NavigationType.VIEW var navigationType = NavigationType.VIEW
var lastTrafficDate = LocalDateTime.of(1960, 6, 21, 0, 0)
val observer = Observer<String> { route -> val observer = Observer<String> { route ->
if (route.isNotEmpty()) { if (route.isNotEmpty()) {
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
@@ -71,6 +78,10 @@ class NavigationScreen(
invalidate() invalidate()
} }
} }
val trafficObserver = Observer<Map<String, String> > { traffic ->
surfaceRenderer.setTrafficData(traffic)
invalidate()
}
val placeObserver = Observer<SearchResult> { searchResult -> val placeObserver = Observer<SearchResult> { searchResult ->
val place = Place( val place = Place(
@@ -101,6 +112,7 @@ class NavigationScreen(
init { init {
viewModel.route.observe(this, observer) viewModel.route.observe(this, observer)
viewModel.traffic.observe(this, trafficObserver);
viewModel.recentPlace.observe(this, recentObserver) viewModel.recentPlace.observe(this, recentObserver)
viewModel.placeLocation.observe(this, placeObserver) viewModel.placeLocation.observe(this, placeObserver)
viewModel.speedCameras.observe(this, speedObserver) viewModel.speedCameras.observe(this, speedObserver)
@@ -471,7 +483,14 @@ class NavigationScreen(
} }
fun updateTrip(location: Location) { fun updateTrip(location: Location) {
updateSpeedCamera(surfaceRenderer.lastLocation) val current = LocalDateTime.now(ZoneOffset.UTC)
val duration = java.time.Duration.between(current, lastTrafficDate)
if (duration.abs().seconds > 360) {
lastTrafficDate = current
viewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
//updateTraffic(location)
updateSpeedCamera(location)
with(routeModel) { with(routeModel) {
updateLocation(location, viewModel) updateLocation(location, viewModel)
if ((maneuverType == Maneuver.TYPE_DESTINATION if ((maneuverType == Maneuver.TYPE_DESTINATION
@@ -512,11 +531,14 @@ class NavigationScreen(
} }
val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.first() val camera = sortedList.first()
val bearingSpeedCamera = location.bearingTo(location(camera.lon!!, camera.lat!!))
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
camera.tags.direction!!.toFloat()
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) { if (camera.distance < 80) {
if ((bearingSpeedCamera.absoluteValue - bearingRoute.absoluteValue).absoluteValue < 20.0) { if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed) routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
} }
} }

View File

@@ -190,7 +190,6 @@ class RoutePreviewScreen(
private fun createRouteText(index: Int): CarText { private fun createRouteText(index: Int): CarText {
val time = routeModel.route.routes[index].summary.duration val time = routeModel.route.routes[index].summary.duration
println("Duration $time")
val length = val length =
BigDecimal(routeModel.route.routes[index].summary.distance).setScale( BigDecimal(routeModel.route.routes[index].summary.distance).setScale(
1, 1,

View File

@@ -23,7 +23,7 @@ import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
class RoutingSettings(private val carContext: CarContext, private var viewModel: ViewModel) : Screen(carContext) { class RoutingSettings(private val carContext: CarContext, private var viewModel: ViewModel) : Screen(carContext) {
private var routingEngine = RouteEngine.VALHALLA.ordinal private var routingEngine = RouteEngine.OSRM.ordinal
init { init {
@@ -45,6 +45,11 @@ class RoutingSettings(private val carContext: CarContext, private var viewModel:
R.string.osrm, R.string.osrm,
) )
) )
.addItem(
buildRowForTemplate(
R.string.tomtom,
)
)
.setOnSelectedListener { index: Int -> .setOnSelectedListener { index: Int ->
this.onSelected(index) this.onSelected(index)
} }

View File

@@ -68,6 +68,7 @@ data class StepData (
var leftDistance: Double, var leftDistance: Double,
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())), var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
var exitNumber: Int = 0,
) )
@@ -150,5 +151,5 @@ object Constants {
enum class RouteEngine { enum class RouteEngine {
VALHALLA, OSRM, GRAPHHOPPER VALHALLA, OSRM, TOMTOM, GRAPHHOPPER
} }

View File

@@ -18,16 +18,13 @@ package com.kouros.navigation.data
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import com.kouros.navigation.data.overpass.Elements import com.kouros.data.R
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
import org.json.JSONArray
import java.net.Authenticator import java.net.Authenticator
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.PasswordAuthentication import java.net.PasswordAuthentication
import java.net.URL import java.net.URL
import kotlinx.serialization.json.Json
abstract class NavigationRepository { abstract class NavigationRepository {
@@ -36,11 +33,29 @@ abstract class NavigationRepository {
private val nominatimUrl = "https://kouros-online.de/nominatim/" private val nominatimUrl = "https://kouros-online.de/nominatim/"
private val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
abstract fun getRoute(currentLocation: Location, location: Location, carOrientation: Float, searchFilter: SearchFilter): String private val tomtomUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails"
fun getRouteDistance(currentLocation: Location, location: Location, carOrientation : Float, searchFilter: SearchFilter, context: Context): Double { private val tomtomFields =
val route = getRoute(currentLocation, location, carOrientation, searchFilter) "{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
abstract fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String
fun getRouteDistance(
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter,
context: Context
): Double {
val route = getRoute(context, currentLocation, location, carOrientation, searchFilter)
val routeModel = RouteModel() val routeModel = RouteModel()
routeModel.startNavigation(route, context) routeModel.startNavigation(route, context)
return routeModel.curRoute.summary.distance return routeModel.curRoute.summary.distance
@@ -48,12 +63,36 @@ abstract class NavigationRepository {
fun searchPlaces(search: String, location: Location): String { fun searchPlaces(search: String, location: Location): String {
val box = calculateSquareRadius(location.latitude, location.longitude, 20.0) val box = calculateSquareRadius(location.latitude, location.longitude, 20.0)
val viewbox = "&bounded=1&viewbox=${box[2]},${box[0]},${box[3]},${box[1]}" val viewbox = "&bounded=1&viewbox=${box}"
return fetchUrl("${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox", false) return fetchUrl(
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
false
)
} }
fun reverseAddress(location: Location): String { fun reverseAddress(location: Location): String {
return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true", false) return fetchUrl(
"${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true",
false
)
}
fun getTraffic(context: Context, location: Location, carOrientation: Float): String {
val useAsset = false
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useAsset) {
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)
trafficJson.bufferedReader().use { it.readText() }
} else {
val trafficResult = fetchUrl(
"$tomtomUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present",
false
)
trafficResult.replace(
"{\"incidents\":",
"{\"type\": \"FeatureCollection\", \"features\":"
)
}
} }
fun fetchUrl(url: String, authenticator: Boolean): String { fun fetchUrl(url: String, authenticator: Boolean): String {
@@ -79,7 +118,7 @@ abstract class NavigationRepository {
val responseCode = httpURLConnection.responseCode val responseCode = httpURLConnection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader() val response = httpURLConnection.inputStream.bufferedReader()
.use { it.readText() } // defaults to UTF-8 .use { it.readText() }
return response return response
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.data package com.kouros.navigation.data
import android.location.Location
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
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
@@ -8,9 +7,10 @@ import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver import com.kouros.navigation.data.route.Maneuver
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.data.tomtom.TomTomResponse
import com.kouros.navigation.data.tomtom.TomTomRoute
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.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
@@ -38,6 +38,7 @@ data class Route(
} }
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine } fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun route(route: String) = apply { fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") { if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
@@ -52,11 +53,14 @@ data class Route(
) )
ValhallaRoute().mapJsonToValhalla(routeJson, this) ValhallaRoute().mapJsonToValhalla(routeJson, this)
} }
RouteEngine.OSRM.ordinal -> {
else -> {
val osrmJson = gson.fromJson(route, OsrmResponse::class.java) val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
OsrmRoute().mapToOsrm(osrmJson, this) OsrmRoute().mapToOsrm(osrmJson, this)
} }
else -> {
val tomtomJson = gson.fromJson(route, TomTomResponse::class.java)
TomTomRoute().mapToOsrm(tomtomJson, this)
}
} }
} }
} }
@@ -71,9 +75,7 @@ data class Route(
fun buildEmpty(): Route { fun buildEmpty(): Route {
return Route( return Route(
routeEngine = 0, routeEngine = 0,
//summary = Summary(0.0, 0.0),
routes = emptyList(), routes = emptyList(),
// legs = emptyList(),
//waypoints = emptyList(), //waypoints = emptyList(),
//routeGeoJson = "", //routeGeoJson = "",
) )
@@ -81,14 +83,18 @@ data class Route(
} }
val legs: List<Leg> fun legs(): List<Leg> {
get() = routes.first().legs return if (routes.isNotEmpty()) {
routes.first().legs
} else {
emptyList()
}
}
fun currentStep(): Step { fun currentStep(): Step {
return if (legs.isNotEmpty()) { return if (routes.isNotEmpty() && legs().isNotEmpty()) {
legs.first().steps[currentStep] legs().first().steps[currentStep]
} else { } else {
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0))) Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
} }
@@ -96,8 +102,8 @@ data class Route(
fun nextStep(): Step { fun nextStep(): Step {
val nextIndex = currentStep + 1 val nextIndex = currentStep + 1
return if (nextIndex < legs.first().steps.size) { return if (nextIndex < legs().first().steps.size) {
legs.first().steps[nextIndex] legs().first().steps[nextIndex]
} else { } else {
throw IndexOutOfBoundsException("No next maneuver available.") throw IndexOutOfBoundsException("No next maneuver available.")
} }

View File

@@ -5,8 +5,8 @@ import com.google.gson.annotations.SerializedName
data class Intersections( data class Intersections(
@SerializedName("in") var inV: Int? = null, @SerializedName("in") var inV: Int = 0,
@SerializedName("out") var out: Int? = null, @SerializedName("out") var out: Int = 0,
@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(),
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(), @SerializedName("location") var location: ArrayList<Double> = arrayListOf(),

View File

@@ -6,9 +6,9 @@ import com.google.gson.annotations.SerializedName
data class Legs ( data class Legs (
@SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(), @SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(),
@SerializedName("weight" ) var weight : Double? = null, @SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("summary" ) var summary : String? = null, @SerializedName("summary" ) var summary : String = "",
@SerializedName("duration" ) var duration : Double? = null, @SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double? = null @SerializedName("distance" ) var distance : Double = 0.0
) )

View File

@@ -5,10 +5,11 @@ import com.google.gson.annotations.SerializedName
data class Maneuver( data class Maneuver(
@SerializedName("bearing_after" ) var bearingAfter : Int? = null, @SerializedName("bearing_after") var bearingAfter: Int = 0,
@SerializedName("bearing_before" ) var bearingBefore : Int? = null, @SerializedName("bearing_before") var bearingBefore: Int = 0,
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(), @SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
@SerializedName("modifier" ) var modifier : String? = null, @SerializedName("modifier") var modifier: String = "",
@SerializedName("type" ) var type : String? = null @SerializedName("type") var type: String = "",
@SerializedName("exit") var exit: Int = 0,
) )

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.osrm package com.kouros.navigation.data.osrm
import android.content.Context
import android.location.Location import android.location.Location
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.SearchFilter
@@ -8,6 +9,7 @@ private const val routeUrl = "https://kouros-online.de/osrm/route/v1/driving/"
class OsrmRepository : NavigationRepository() { class OsrmRepository : NavigationRepository() {
override fun getRoute( override fun getRoute(
context: Context,
currentLocation: Location, currentLocation: Location,
location: Location, location: Location,
carOrientation: Float, carOrientation: Float,

View File

@@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
data class OsrmResponse ( data class OsrmResponse (
@SerializedName("code" ) var code : String? = null, @SerializedName("code" ) var code : String = "",
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(), @SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf() @SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()

View File

@@ -16,25 +16,24 @@ class OsrmRoute {
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) { fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) {
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>() val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
var stepIndex = 0 var stepIndex = 0
routeJson.routes.forEach { route -> routeJson.routes.forEach { route ->
val legs = mutableListOf<Leg>() val legs = mutableListOf<Leg>()
val waypoints = mutableListOf<List<Double>>() val waypoints = mutableListOf<List<Double>>()
val summary = Summary(route.duration!!, route.distance!! / 1000) val summary = Summary(route.duration, route.distance / 1000)
route.legs.forEach { leg -> route.legs.forEach { leg ->
val steps = mutableListOf<Step>() val steps = mutableListOf<Step>()
leg.steps.forEach { step -> leg.steps.forEach { step ->
val intersections = mutableListOf<Intersection>() val intersections = mutableListOf<Intersection>()
if (step.maneuver != null) { val points = decodePolyline(step.geometry, 5)
val points = decodePolyline(step.geometry!!, 5)
waypoints.addAll(points) waypoints.addAll(points)
val maneuver = RouteManeuver( val maneuver = RouteManeuver(
bearingBefore = step.maneuver.bearingBefore ?: 0, bearingBefore = step.maneuver.bearingBefore,
bearingAfter = step.maneuver.bearingAfter ?: 0, bearingAfter = step.maneuver.bearingAfter,
type = convertType(step.maneuver), type = convertType(step.maneuver),
waypoints = points, waypoints = points,
exit = step.maneuver.exit,
location = location( location = location(
step.maneuver.location[0], step.maneuver.location[0],
step.maneuver.location[1] step.maneuver.location[1]
@@ -58,16 +57,15 @@ class OsrmRoute {
} }
val step = Step( val step = Step(
index = stepIndex, index = stepIndex,
name = step.name!!, name = step.name,
distance = step.distance!! / 1000, distance = step.distance / 1000,
duration = step.duration!!, duration = step.duration,
maneuver = maneuver, maneuver = maneuver,
intersection = intersections intersection = intersections
) )
steps.add(step) steps.add(step)
stepIndex += 1 stepIndex += 1
} }
}
legs.add(Leg(steps)) legs.add(Leg(steps))
} }
val routeGeoJson = createLineStringCollection(waypoints) val routeGeoJson = createLineStringCollection(waypoints)

View File

@@ -8,8 +8,8 @@ data class Routes (
@SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(), @SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
@SerializedName("weight_name" ) var weightName : String? = null, @SerializedName("weight_name" ) var weightName : String? = null,
@SerializedName("geometry" ) var geometry : String? = null, @SerializedName("geometry" ) var geometry : String? = null,
@SerializedName("weight" ) var weight : Double? = null, @SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("duration" ) var duration : Double? = null, @SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double? = null @SerializedName("distance" ) var distance : Double = 0.0
) )

View File

@@ -6,13 +6,13 @@ import com.google.gson.annotations.SerializedName
data class Steps ( data class Steps (
@SerializedName("intersections" ) var intersections : ArrayList<Intersections> = arrayListOf(), @SerializedName("intersections" ) var intersections : ArrayList<Intersections> = arrayListOf(),
@SerializedName("driving_side" ) var drivingSide : String? = null, @SerializedName("driving_side" ) var drivingSide : String = "",
@SerializedName("geometry" ) var geometry : String? = null, @SerializedName("geometry" ) var geometry : String = "",
@SerializedName("maneuver" ) val maneuver : Maneuver? = Maneuver(), @SerializedName("maneuver" ) val maneuver : Maneuver = Maneuver(),
@SerializedName("name" ) var name : String? = null, @SerializedName("name" ) var name : String = "",
@SerializedName("mode" ) var mode : String? = null, @SerializedName("mode" ) var mode : String = "",
@SerializedName("weight" ) var weight : Double? = null, @SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("duration" ) var duration : Double? = null, @SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double? = null @SerializedName("distance" ) var distance : Double = 0.0,
) )

View File

@@ -5,9 +5,9 @@ import com.google.gson.annotations.SerializedName
data class Waypoints ( data class Waypoints (
@SerializedName("hint" ) var hint : String? = null, @SerializedName("hint" ) var hint : String = "",
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(), @SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
@SerializedName("name" ) var name : String? = null, @SerializedName("name" ) var name : String = "",
@SerializedName("distance" ) var distance : Double? = null @SerializedName("distance" ) var distance : Double = 0.0,
) )

View File

@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
data class Elements ( data class Elements (
@SerializedName("type" ) var type : String? = null, @SerializedName("type" ) var type : String = "",
@SerializedName("id" ) var id : Long? = null, @SerializedName("id" ) var id : Long = 0,
@SerializedName("lat" ) var lat : Double? = null, @SerializedName("lat" ) var lat : Double = 0.0,
@SerializedName("lon" ) var lon : Double? = null, @SerializedName("lon" ) var lon : Double = 0.0,
@SerializedName("tags" ) var tags : Tags = Tags(), @SerializedName("tags" ) var tags : Tags = Tags(),
var distance : Double = 0.0 var distance : Double = 0.0

View File

@@ -2,8 +2,7 @@ package com.kouros.navigation.data.overpass
import android.location.Location import android.location.Location
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.utils.GeoUtils.getOverpassBbox import com.kouros.navigation.utils.GeoUtils.getBoundingBox
import kotlinx.serialization.json.Json
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
@@ -41,7 +40,7 @@ class Overpass {
location: Location, location: Location,
radius: Double radius: Double
): List<Elements> { ): List<Elements> {
val boundingBox = getOverpassBbox(location, radius) val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST" httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty( httpURLConnection.setRequestProperty(

View File

@@ -18,7 +18,7 @@ data class Tags(
@SerializedName("ref") var ref: String? = null, @SerializedName("ref") var ref: String? = null,
@SerializedName("socket:type2") var socketType2: String? = null, @SerializedName("socket:type2") var socketType2: String? = null,
@SerializedName("socket:type2:output") var socketType2Output: String? = null, @SerializedName("socket:type2:output") var socketType2Output: String? = null,
@SerializedName("maxspeed") var maxspeed: String? = null, @SerializedName("maxspeed") var maxspeed: String = "0",
@SerializedName("direction") var direction: String? = null, @SerializedName("direction") var direction: String? = null,
) )

View File

@@ -8,4 +8,5 @@ data class Maneuver(
val type: Int = 0, val type: Int = 0,
val waypoints: List<List<Double>>, val waypoints: List<List<Double>>,
val location: Location, val location: Location,
val exit: Int = 0,
) )

View File

@@ -0,0 +1,10 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Events (
@SerializedName("description" ) var description : String? = null
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Features (
@SerializedName("type" ) var type : String? = null,
@SerializedName("properties" ) var properties : Properties? = Properties(),
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
)

View File

@@ -0,0 +1,11 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Geometry (
@SerializedName("type" ) var type : String? = null,
@SerializedName("coordinates" ) var coordinates : List<List<Double>> = arrayListOf()
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Incidents (
@SerializedName("type" ) var type : String? = null,
@SerializedName("properties" ) var properties : Properties? = Properties(),
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
)

View File

@@ -0,0 +1,11 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Properties (
@SerializedName("iconCategory" ) var iconCategory : Int? = null,
@SerializedName("events" ) var events : ArrayList<Events> = arrayListOf()
)

View File

@@ -0,0 +1,5 @@
package com.kouros.navigation.data.tomtom
data class Report(
val effectiveSettings: List<EffectiveSetting>
)

View File

@@ -0,0 +1,72 @@
package com.kouros.navigation.data.tomtom
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute.ManeuverType
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
/**
curl -X GET "https://api.tomtom.com/routing/1/calculateRoute/\
48.1856548,11.57928:48.1183,11.59485/json?\
vehicleHeading=90&sectionType=traffic\
&report=effectiveSettings&routeType=eco\
&traffic=true&avoid=unpavedRoadimport com.kouros.navigation.data.route.Maneuver as RouteManeuvers&travelMode=car\
&vehicleMaxSpeed=120&vehicleCommercial=false\
&instructionsType=text&language=en-GB&sectionType=lanes\
&routeRepresentation=encodedPolyline\
&vehicleEngineType=combustion&key=678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
*/
class TomTomRoute {
fun mapToOsrm(routeJson: TomTomResponse, builder: Route.Builder) {
routeJson.routes.forEach { route ->
val legs = mutableListOf<Leg>()
val waypoints = mutableListOf<List<Double>>()
var points = listOf<List<Double>>()
val summary = Summary(
route.summary.travelTimeInSeconds.toDouble(),
route.summary.lengthInMeters.toDouble() / 1000
)
route.legs.forEach { leg ->
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
waypoints.addAll(points)
}
route.guidance.instructions.forEach { instruction ->
instruction.exitNumber
// val maneuver = RouteManeuver(
// // bearingBefore = step.maneuver.bearingBefore,
// //bearingAfter = step.maneuver.bearingAfter,
// type = convertType(instruction.maneuver),
// waypoints = points.subList(section.startPointIndex, section.endPointIndex + 1),
// exit = instruction.exitNumber.toInt(),
// location = location(
// instruction.point.longitude, instruction.point.latitude
// )
// )
}
route.sections.forEach { section ->
}
}
println(routeJson)
}
fun convertType(type: String): Int {
var newType = 0
when (type) {
"DEPART" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
}
}
return newType
}
}

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Traffic (
//@SerializedName("incidents" ) var incidents : ArrayList<Incidents> = arrayListOf()
@SerializedName("type" ) var type : String = "",
@SerializedName("features" ) var features : ArrayList<Features> = arrayListOf()
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class TrafficData (
var traffic : Traffic ,
var trafficData: String = ""
)

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.valhalla package com.kouros.navigation.data.valhalla
import android.content.Context
import android.location.Location import android.location.Location
import com.kouros.navigation.data.Locations import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
@@ -13,6 +14,7 @@ private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
class ValhallaRepository : NavigationRepository() { class ValhallaRepository : NavigationRepository() {
override fun getRoute( override fun getRoute(
context: Context,
currentLocation: Location, currentLocation: Location,
location: Location, location: Location,
carOrientation: Float, carOrientation: Float,

View File

@@ -21,6 +21,7 @@ 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.Routes import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.data.valhalla.ManeuverType import com.kouros.navigation.data.valhalla.ManeuverType
import com.kouros.navigation.utils.Levenshtein
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
@@ -128,16 +129,24 @@ open class RouteModel() {
fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking { fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val instruction = currentStep().instruction
val levenshtein = Levenshtein()
// speed limit // speed limit
val distance = lastSpeedLocation.distanceTo(location) val distance = lastSpeedLocation.distanceTo(location)
if (distance > 500 || lastSpeedIndex < route.currentStep) { if (distance > 500 || lastSpeedIndex < route.currentStep) {
lastSpeedIndex = route.currentStep lastSpeedIndex = route.currentStep
lastSpeedLocation = location
val elements = viewModel.getMaxSpeed(location) val elements = viewModel.getMaxSpeed(location)
elements.forEach { elements.forEach {
if (it.tags.name != null && it.tags.maxspeed != null) { if (it.tags.name != null) {
val speed = it.tags.maxspeed!!.toInt() if (isNavigating()) {
val distance =
levenshtein.distance(it.tags.name!!, instruction)
if (distance < 5) {
val speed = it.tags.maxspeed.toInt()
maxSpeed = speed maxSpeed = speed
lastSpeedLocation = location
}
}
} }
} }
} }
@@ -150,7 +159,7 @@ open class RouteModel() {
val distanceToNextStep = leftStepDistance() val distanceToNextStep = leftStepDistance()
val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD
val shouldAdvance = val shouldAdvance =
isNearNextManeuver && route.currentStep < (route.legs.first().steps.size) isNearNextManeuver && route.currentStep < (route.legs().first().steps.size)
// Determine the maneuver type and corresponding icon // Determine the maneuver type and corresponding icon
var curManeuverType = if (hasArrived(currentStep.maneuver.type)) { var curManeuverType = if (hasArrived(currentStep.maneuver.type)) {
@@ -166,17 +175,16 @@ open class RouteModel() {
} }
// Safely get the street name from the maneuver // Safely get the street name from the maneuver
val streetName = relevantStep.name val streetName = relevantStep.name
var exitNumber = currentStep.maneuver.exit
if (shouldAdvance) { if (shouldAdvance) {
curManeuverType = relevantStep.maneuver.type curManeuverType = relevantStep.maneuver.type
exitNumber = relevantStep.maneuver.exit
} }
val maneuverIcon = maneuverIcon(curManeuverType) val maneuverIcon = maneuverIcon(curManeuverType)
maneuverType = curManeuverType maneuverType = curManeuverType
val lanes = currentLanes(location) val lanes = currentLanes(location)
if (lanes.isNotEmpty())
println("Street: $streetName Dist: $distanceToNextStep Lane: ${lanes.size}")
// Construct and return the final StepData object // Construct and return the final StepData object
return StepData( return StepData(
streetName, streetName,
@@ -185,9 +193,9 @@ open class RouteModel() {
maneuverIcon, maneuverIcon,
arrivalTime(), arrivalTime(),
travelLeftDistance(), travelLeftDistance(),
lanes lanes,
exitNumber
) )
} }
@@ -215,7 +223,10 @@ open class RouteModel() {
maneuverType, maneuverType,
maneuverIcon, maneuverIcon,
arrivalTime(), arrivalTime(),
travelLeftDistance() travelLeftDistance(),
listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
step.maneuver.exit
) )
} }
@@ -263,7 +274,7 @@ open class RouteModel() {
fun travelLeftDistance(): Double { fun travelLeftDistance(): Double {
var leftDistance = 0.0 var leftDistance = 0.0
for (i in route.currentStep + 1..<curLeg.steps.size) { for (i in route.currentStep + 1..<curLeg.steps.size) {
val step = route.legs!![0].steps[i] val step = route.legs()[0].steps[i]
leftDistance += step.distance leftDistance += step.distance
} }
leftDistance += leftStepDistance() / 1000 leftDistance += leftStepDistance() / 1000

View File

@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox.boxStore import com.kouros.navigation.data.ObjectBox.boxStore
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
@@ -19,12 +18,16 @@ import com.kouros.navigation.data.nominatim.Search
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.data.overpass.Overpass import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.data.tomtom.Features
import com.kouros.navigation.data.tomtom.Traffic
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.NavigationUtils import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import io.objectbox.kotlin.boxFor import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.maplibre.geojson.FeatureCollection
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -34,6 +37,11 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData() MutableLiveData()
} }
val traffic: MutableLiveData<Map<String, String> > by lazy {
MutableLiveData()
}
val previewRoute: MutableLiveData<String> by lazy { val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData() MutableLiveData()
} }
@@ -156,6 +164,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
try { try {
route.postValue( route.postValue(
repository.getRoute( repository.getRoute(
context,
currentLocation, currentLocation,
location, location,
carOrientation, carOrientation,
@@ -168,11 +177,46 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
} }
} }
fun loadTraffic(context: Context, currentLocation: Location, carOrientation : Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
val data = repository.getTraffic(
context,
currentLocation,
carOrientation
)
val trafficData = rebuildTraffic(data)
traffic.postValue(
trafficData
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun rebuildTraffic(data: String) : Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Queuing traffic")}
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
val stationary = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Stationary traffic")}
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
val slow = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Slow traffic")}
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
val heavy = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Heavy traffic")}
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
val roadworks = featureCollection.features()!!.filter { it.properties()!!.get("events").toString().contains("Roadworks")}
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
return incidents
}
fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location, carOrientation: Float) { fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
previewRoute.postValue( previewRoute.postValue(
repository.getRoute( repository.getRoute(
context,
currentLocation, currentLocation,
location, location,
carOrientation, carOrientation,

View File

@@ -115,45 +115,31 @@ object GeoUtils {
return featureCollection.toJson() return featureCollection.toJson()
} }
fun getOverpassBbox(location: Location, radius: Double): String {
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
val neLon = bbox["ne"]?.get("lon")
val neLat = bbox["ne"]?.get("lat")
val swLon = bbox["sw"]?.get("lon")
val swLat = bbox["sw"]?.get("lat")
return "$swLon,$swLat,$neLon,$neLat"
}
/** /**
* Calculate the lat and len of a square around a point. * Calculate the lat and len of a square around a point.
* @return latMin, latMax, lngMin, lngMax * @return latMin, latMax, lngMin, lngMax
*/ */
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): DoubleArray { fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
val earthRadius = 6371.0 // earth radius in km val earthRadius = 6371.0 // earth radius in km
val latMin = lat - toDegrees(radius / earthRadius) val latMin = lat - toDegrees(radius / earthRadius)
val latMax = lat + toDegrees(radius / earthRadius) val latMax = lat + toDegrees(radius / earthRadius)
val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat))) val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat)))
val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat))) val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat)))
return doubleArrayOf(latMin, latMax, lngMin, lngMax) return "$lngMin,$latMin,$lngMax,$latMax"
} }
fun getBoundingBox( fun getBoundingBox(
lat: Double, lat: Double,
lon: Double, lon: Double,
radius: Double radius: Double
): Map<String, Map<String, Double>> { ): String {
val earthRadius = 6371.0 val earthRadius = 6371.0
val maxLat = lat + toDegrees(radius / earthRadius) val maxLat = lat + toDegrees(radius / earthRadius)
val minLat = lat - toDegrees(radius / earthRadius) val minLat = lat - toDegrees(radius / earthRadius)
val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat))) val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat))) val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
return mapOf( return "$minLat,$minLon,$maxLat,$maxLon"
"nw" to mapOf("lat" to maxLat, "lon" to minLon),
"ne" to mapOf("lat" to maxLat, "lon" to maxLon),
"sw" to mapOf("lat" to minLat, "lon" to minLon),
"se" to mapOf("lat" to minLat, "lon" to maxLon)
)
} }
} }

View File

@@ -0,0 +1,63 @@
package com.kouros.navigation.utils
import kotlin.math.min
/**
* The Levenshtein distance between two words is the minimum number of single-character edits (insertions, deletions or
* substitutions) required to change one string into the other.
*
* This implementation uses dynamic programming (WagnerFischer algorithm).
*
* [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance)
*/
class Levenshtein {
/**
* The Levenshtein distance, or edit distance, between two words is the minimum number of single-character edits
* (insertions, deletions or substitutions) required to change one word into the other.
*
* It is always at least the difference of the sizes of the two strings.
* It is at most the length of the longer string.
* It is `0` if and only if the strings are equal.
*
* @param first first string to compare.
* @param second second string to compare.
* @param limit the maximum result to compute before stopping, terminating calculation early.
* @return the computed Levenshtein distance.
*/
fun distance(first: CharSequence, second: CharSequence, limit: Int = Int.MAX_VALUE): Int {
if (first == second) return 0
if (first.isEmpty()) return second.length
if (second.isEmpty()) return first.length
// initial costs is the edit distance from an empty string, which corresponds to the characters to inserts.
// the array size is : length + 1 (empty string)
var cost = IntArray(first.length + 1) { it }
var newCost = IntArray(first.length + 1)
for (i in 1..second.length) {
// calculate new costs from the previous row.
// the first element of the new row is the edit distance (deletes) to match empty string
newCost[0] = i
var minCost = i
// fill in the rest of the row
for (j in 1..first.length) {
// if it's the same char at the same position, no edit cost.
val edit = if (first[j - 1] == second[i - 1]) 0 else 1
val replace = cost[j - 1] + edit
val insert = cost[j] + 1
val delete = newCost[j - 1] + 1
newCost[j] = minOf(insert, delete, replace)
minCost = min(minCost, newCost[j])
}
if (minCost >= limit) return limit
// flip references of current and previous row
val swap = cost
cost = newCost
newCost = swap
}
return cost.last()
}
}

View File

@@ -8,6 +8,7 @@ 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
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -29,7 +30,8 @@ object NavigationUtils {
val routeEngine = getIntKeyValue(context = context, ROUTING_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()) RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
else -> ViewModel(TomTomRepository())
} }
} }

View File

@@ -69,17 +69,7 @@
"source-layer": "landuse", "source-layer": "landuse",
"maxzoom": 12, "maxzoom": 12,
"filter": ["==", ["get", "class"], "residential"], "filter": ["==", ["get", "class"], "residential"],
"paint": { "paint": {"fill-color": "rgba(48, 43, 57, 1)"}
"fill-color": [
"interpolate",
["linear"],
["zoom"],
9,
"hsla(0,3%,85%,0.84)",
12,
"hsla(35,57%,88%,0.49)"
]
}
}, },
{ {
"id": "landcover_wood", "id": "landcover_wood",
@@ -89,7 +79,7 @@
"filter": ["==", ["get", "class"], "wood"], "filter": ["==", ["get", "class"], "wood"],
"paint": { "paint": {
"fill-antialias": false, "fill-antialias": false,
"fill-color": "hsla(98,61%,72%,0.7)", "fill-color": "rgba(21, 28, 16, 0.7)",
"fill-opacity": 0.4 "fill-opacity": 0.4
} }
}, },
@@ -1303,15 +1293,7 @@
], ],
"layout": {"line-cap": "round", "line-join": "round"}, "layout": {"line-cap": "round", "line-join": "round"},
"paint": { "paint": {
"line-color": [ "line-color": "rgba(12, 84, 84, 1)",
"interpolate",
["linear"],
["zoom"],
5,
"hsl(26,87%,62%)",
6,
"#ab9"
],
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],
@@ -2457,7 +2439,8 @@
"text-field": ["to-string", ["get", "ref"]], "text-field": ["to-string", ["get", "ref"]],
"text-font": ["Noto Sans Regular"], "text-font": ["Noto Sans Regular"],
"text-rotation-alignment": "viewport", "text-rotation-alignment": "viewport",
"text-size": 10 "text-size": 10,
"visibility": "none"
} }
}, },
{ {
@@ -2585,7 +2568,8 @@
"text-letter-spacing": 0.1, "text-letter-spacing": 0.1,
"text-max-width": 9, "text-max-width": 9,
"text-size": ["interpolate", ["linear"], ["zoom"], 8, 9, 12, 10], "text-size": ["interpolate", ["linear"], ["zoom"], 8, 9, 12, 10],
"text-transform": "uppercase" "text-transform": "uppercase",
"visibility": "none"
}, },
"paint": { "paint": {
"text-color": "#333", "text-color": "#333",

View File

@@ -0,0 +1,603 @@
{
"formatVersion": "0.0.12",
"report": {
"effectiveSettings": [
{
"key": "avoid",
"value": "unpavedRoads"
},
{
"key": "computeBestOrder",
"value": "false"
},
{
"key": "computeTollAmounts",
"value": "none"
},
{
"key": "computeTravelTimeFor",
"value": "none"
},
{
"key": "contentType",
"value": "json"
},
{
"key": "departAt",
"value": "2026-01-29T08:43:35.397Z"
},
{
"key": "guidanceVersion",
"value": "1"
},
{
"key": "includeTollPaymentTypes",
"value": "none"
},
{
"key": "instructionsType",
"value": "text"
},
{
"key": "language",
"value": "en-GB"
},
{
"key": "locations",
"value": "48.18565,11.57928:48.11830,11.59485"
},
{
"key": "maxAlternatives",
"value": "0"
},
{
"key": "routeRepresentation",
"value": "encodedPolyline"
},
{
"key": "routeType",
"value": "eco"
},
{
"key": "sectionType",
"value": "lanes"
},
{
"key": "sectionType",
"value": "traffic"
},
{
"key": "traffic",
"value": "true"
},
{
"key": "travelMode",
"value": "car"
},
{
"key": "vehicleAxleWeight",
"value": "0"
},
{
"key": "vehicleCommercial",
"value": "false"
},
{
"key": "vehicleEngineType",
"value": "combustion"
},
{
"key": "vehicleHeading",
"value": "90"
},
{
"key": "vehicleHeight",
"value": "0.00"
},
{
"key": "vehicleLength",
"value": "0.00"
},
{
"key": "vehicleMaxSpeed",
"value": "120"
},
{
"key": "vehicleNumberOfAxles",
"value": "0"
},
{
"key": "vehicleWeight",
"value": "0"
},
{
"key": "vehicleWidth",
"value": "0.00"
}
]
},
"routes": [
{
"summary": {
"lengthInMeters": 10879,
"travelTimeInSeconds": 1170,
"trafficDelayInSeconds": 76,
"trafficLengthInMeters": 1727,
"departureTime": "2026-01-29T09:43:35+01:00",
"arrivalTime": "2026-01-29T10:03:05+01:00"
},
"legs": [
{
"summary": {
"lengthInMeters": 10879,
"travelTimeInSeconds": 1170,
"trafficDelayInSeconds": 76,
"trafficLengthInMeters": 1727,
"departureTime": "2026-01-29T09:43:35+01:00",
"arrivalTime": "2026-01-29T10:03:05+01:00"
},
"encodedPolyline": "sfbeHmqteAEjDQEy@GQ?wDQFkEH{G?M?sA@kB?_FAeC?o@?[@_@\\Ab@CVAz@CJA@?dBGhAGjAExAGlBEvBKdCKTAfCKLAv@ELA|AGnAGt@ClCKjDQpBIf@BXDPDPBF@ZB?S@SAYAUKi@Go@Cc@BgBBs@Bg@JyAJiAXqBDWNs@TaA\\mAFa@Po@`BwF?YL]FSFSl@iB^kALc@L]Ro@f@yAf@{AFQNe@dAoCdBgEx@qBTi@BITe@L[L_@^}@HSXu@pB}El@eAb@e@f@[h@QZCRAL?j@HRFh@Vf@XLJhAn@lAv@TLtAz@r@`@bAn@pAv@f@X~@j@z@h@vBpA`@VHDFFJFf@X`CzApAh@f@L`@Fz@@f@AXEVEVEhA[h@Yn@e@PQFEJKRWV[PW`@w@t@}AHQN]~BiFP]`AoBh@aADGTa@t@aAt@{@PQJKJGFG@Cd@]XSxDmCf@a@n@o@TY\\g@LQHMJSLUP[f@iAPg@b@yAFODMNi@FS|@qCVaAHUHUn@wBHYh@eBpAkEjBiGfAeDj@yADMFQd@sAf@kAJUh@qAf@eAt@sAn@iALSN[p@kAVc@JOLSj@w@z@}@x@q@pAu@p@_@j@Sl@MLCRCb@E`@?^?L@`ABz@?N@~AFdADJ@rAH`DVpCVrAJd@BfHp@zGl@pAJ|ALnGp@jEh@fBJpAFF?P@N@ZCtC]r@GJCFCLCD?TEVEXGhAYzAg@NGv@]`@QJEvB_AXMVK\\Qb@Qn@QJCNAZC^ENA`@FnBb@xEtA^H^JnCl@z@r@`@Pr@TtBlA~C`Bn@\\xAl@PF`@LrAVlCh@bBLl@BlBJdG\\RDjCHn@?pB?xB?R@`@@GxAC^?ZInBIfCAvC?r@@dD@n@@b@@^D`C?TDxAFbBHdB@VHp@RjAJb@NNH`@VlBFf@PzARhBFd@@LRbBFh@\\nC@FNhAb@lEj@tDPpABTBRZlBTdBXjBn@xEBLDTRpAR~@Fb@",
"encodedPolylinePrecision": 5
}
],
"sections": [
{
"startPointIndex": 83,
"endPointIndex": 147,
"sectionType": "TRAFFIC",
"simpleCategory": "JAM",
"effectiveSpeedInKmh": 35,
"delayInSeconds": 76,
"magnitudeOfDelay": 1,
"tec": {
"causes": [
{
"mainCauseCode": 1
}
],
"effectCode": 4
},
"eventId": "TTL41048054144049000"
},
{
"lanes": [
{
"directions": [
"SLIGHT_LEFT"
],
"follow": "SLIGHT_LEFT"
},
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"STRAIGHT"
]
}
],
"laneSeparators": [
"SINGLE_SOLID",
"SINGLE_SOLID",
"LONG_DASHED",
"LONG_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 42,
"endPointIndex": 45,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"STRAIGHT"
],
"follow": "STRAIGHT"
},
{
"directions": [
"SLIGHT_RIGHT"
]
}
],
"laneSeparators": [
"SINGLE_SOLID",
"SHORT_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 61,
"endPointIndex": 62,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"SLIGHT_LEFT"
],
"follow": "SLIGHT_LEFT"
},
{
"directions": [
"SLIGHT_LEFT"
],
"follow": "SLIGHT_LEFT"
},
{
"directions": [
"SLIGHT_RIGHT"
]
}
],
"laneSeparators": [
"SINGLE_SOLID",
"LONG_DASHED",
"SHORT_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 74,
"endPointIndex": 75,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"SLIGHT_RIGHT"
],
"follow": "SLIGHT_RIGHT"
}
],
"laneSeparators": [
"SINGLE_SOLID",
"LONG_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 265,
"endPointIndex": 266,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"LEFT"
]
},
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"STRAIGHT"
]
},
{
"directions": [
"RIGHT"
],
"follow": "RIGHT"
}
],
"laneSeparators": [
"SINGLE_SOLID",
"SINGLE_SOLID",
"SINGLE_SOLID",
"SINGLE_SOLID",
"SINGLE_SOLID"
],
"startPointIndex": 287,
"endPointIndex": 288,
"sectionType": "LANES"
},
{
"lanes": [
{
"directions": [
"LEFT"
]
},
{
"directions": [
"STRAIGHT"
],
"follow": "STRAIGHT"
},
{
"directions": [
"STRAIGHT"
],
"follow": "STRAIGHT"
},
{
"directions": [
"RIGHT"
]
}
],
"laneSeparators": [
"SINGLE_SOLID",
"SHORT_DASHED",
"LONG_DASHED",
"SHORT_DASHED",
"SINGLE_SOLID"
],
"startPointIndex": 302,
"endPointIndex": 304,
"sectionType": "LANES"
}
],
"guidance": {
"instructions": [
{
"routeOffsetInMeters": 0,
"travelTimeInSeconds": 0,
"point": {
"latitude": 48.18554,
"longitude": 11.57927
},
"exitNumber": "",
"pointIndex": 0,
"instructionType": "LOCATION_DEPARTURE",
"street": "Vogelhartstraße",
"countryCode": "DEU",
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "DEPART",
"message": "Leave from Vogelhartstraße"
},
{
"routeOffsetInMeters": 64,
"travelTimeInSeconds": 14,
"point": {
"latitude": 48.18557,
"longitude": 11.57841
},
"pointIndex": 1,
"instructionType": "TURN",
"street": "Silcherstraße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 90,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "TURN_RIGHT",
"message": "Turn right onto Silcherstraße"
},
{
"routeOffsetInMeters": 218,
"travelTimeInSeconds": 57,
"point": {
"latitude": 48.18696,
"longitude": 11.57857
},
"pointIndex": 5,
"instructionType": "TURN",
"street": "Schmalkaldener Straße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 90,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "TURN_RIGHT",
"message": "Turn right onto Schmalkaldener Straße"
},
{
"routeOffsetInMeters": 650,
"travelTimeInSeconds": 131,
"point": {
"latitude": 48.18686,
"longitude": 11.58437
},
"pointIndex": 15,
"instructionType": "TURN",
"roadNumbers": [
"B13"
],
"street": "Ingolstädter Straße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 90,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "TURN_RIGHT",
"message": "Turn right onto Ingolstädter Straße/B13"
},
{
"routeOffsetInMeters": 1713,
"travelTimeInSeconds": 266,
"point": {
"latitude": 48.17733,
"longitude": 11.58503
},
"pointIndex": 45,
"instructionType": "TURN",
"roadNumbers": [
"B2R"
],
"street": "Schenkendorfstraße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": -90,
"possibleCombineWithNext": true,
"drivingSide": "RIGHT",
"maneuver": "TURN_LEFT",
"message": "Turn left onto Schenkendorfstraße/B2R",
"combinedMessage": "Turn left onto Schenkendorfstraße/B2R then keep left at Schenkendorfstraße/B2R toward Messe / ICM"
},
{
"routeOffsetInMeters": 2067,
"travelTimeInSeconds": 309,
"point": {
"latitude": 48.17678,
"longitude": 11.58957
},
"pointIndex": 62,
"instructionType": "TURN",
"roadNumbers": [
"B2R"
],
"street": "Schenkendorfstraße",
"countryCode": "DEU",
"signpostText": "Messe / ICM",
"junctionType": "BIFURCATION",
"turnAngleInDecimalDegrees": -45,
"possibleCombineWithNext": true,
"drivingSide": "RIGHT",
"maneuver": "KEEP_LEFT",
"message": "Keep left at Schenkendorfstraße/B2R toward Messe / ICM",
"combinedMessage": "Keep left at Schenkendorfstraße/B2R toward Messe / ICM then keep left at Schenkendorfstraße/B2R toward Passau"
},
{
"routeOffsetInMeters": 2419,
"travelTimeInSeconds": 332,
"point": {
"latitude": 48.17518,
"longitude": 11.59363
},
"pointIndex": 75,
"instructionType": "TURN",
"roadNumbers": [
"B2R"
],
"street": "Schenkendorfstraße",
"countryCode": "DEU",
"signpostText": "Passau",
"junctionType": "BIFURCATION",
"turnAngleInDecimalDegrees": -45,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "KEEP_LEFT",
"message": "Keep left at Schenkendorfstraße/B2R toward Passau"
},
{
"routeOffsetInMeters": 2774,
"travelTimeInSeconds": 357,
"point": {
"latitude": 48.17329,
"longitude": 11.59747
},
"pointIndex": 86,
"instructionType": "DIRECTION_INFO",
"roadNumbers": [
"B2R"
],
"street": "Isarring",
"countryCode": "DEU",
"signpostText": "München-Ost",
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "FOLLOW",
"message": "Follow Isarring/B2R toward München-Ost"
},
{
"routeOffsetInMeters": 8425,
"travelTimeInSeconds": 806,
"point": {
"latitude": 48.13017,
"longitude": 11.61541
},
"pointIndex": 266,
"instructionType": "TURN",
"street": "Ampfingstraße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 45,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "BEAR_RIGHT",
"message": "Bear right at Ampfingstraße"
},
{
"routeOffsetInMeters": 9487,
"travelTimeInSeconds": 953,
"point": {
"latitude": 48.12089,
"longitude": 11.61285
},
"pointIndex": 288,
"instructionType": "TURN",
"street": "Anzinger Straße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 90,
"possibleCombineWithNext": true,
"drivingSide": "RIGHT",
"maneuver": "TURN_RIGHT",
"message": "Turn right onto Anzinger Straße",
"combinedMessage": "Turn right onto Anzinger Straße then keep straight on at Sankt-Martin-Straße"
},
{
"routeOffsetInMeters": 9983,
"travelTimeInSeconds": 1044,
"point": {
"latitude": 48.12087,
"longitude": 11.60621
},
"pointIndex": 304,
"instructionType": "TURN",
"street": "Sankt-Martin-Straße",
"countryCode": "DEU",
"junctionType": "REGULAR",
"turnAngleInDecimalDegrees": 0,
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "STRAIGHT",
"message": "Keep straight on at Sankt-Martin-Straße"
},
{
"routeOffsetInMeters": 10879,
"travelTimeInSeconds": 1170,
"point": {
"latitude": 48.1183,
"longitude": 11.59485
},
"pointIndex": 335,
"instructionType": "LOCATION_ARRIVAL",
"street": "Sankt-Martin-Straße",
"countryCode": "DEU",
"possibleCombineWithNext": false,
"drivingSide": "RIGHT",
"maneuver": "ARRIVE",
"message": "You have arrived at Sankt-Martin-Straße"
}
],
"instructionGroups": [
{
"firstInstructionIndex": 0,
"lastInstructionIndex": 3,
"groupMessage": "Leave from Vogelhartstraße. Take the Ingolstädter Straße/B13",
"groupLengthInMeters": 1713
},
{
"firstInstructionIndex": 4,
"lastInstructionIndex": 7,
"groupMessage": "Take the Schenkendorfstraße, Isarring/B2R toward Messe / ICM, Passau, München-Ost",
"groupLengthInMeters": 6712
},
{
"firstInstructionIndex": 8,
"lastInstructionIndex": 11,
"groupMessage": "Take the Ampfingstraße, Anzinger Straße. Continue to your destination at Sankt-Martin-Straße",
"groupLengthInMeters": 2454
}
]
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -48,5 +48,6 @@
<string name="charging_station">Ladestation</string> <string name="charging_station">Ladestation</string>
<string name="speed_camera">Speed camera</string> <string name="speed_camera">Speed camera</string>
<string name="use_car_location">Auto GPS verwenden</string> <string name="use_car_location">Auto GPS verwenden</string>
<string name="tomtom">TomTom\t</string>
</resources> </resources>

View File

@@ -34,4 +34,5 @@
<string name="osrm" translatable="false">Osrm</string> <string name="osrm" translatable="false">Osrm</string>
<string name="routing_engine" translatable="false">Routing engine</string> <string name="routing_engine" translatable="false">Routing engine</string>
<string name="use_car_location">Use car location</string> <string name="use_car_location">Use car location</string>
<string name="tomtom">TomTom\t</string>
</resources> </resources>