Refactoring Route, Speed

This commit is contained in:
Dimitris
2025-12-29 15:44:52 +01:00
parent 1b8abbd4eb
commit 82027dce76
32 changed files with 350 additions and 134 deletions

View File

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

View File

@@ -2,22 +2,34 @@ package com.kouros.navigation
import android.app.Application
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.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.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.setIntKeyValue
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import org.maplibre.compose.expressions.dsl.switch
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
ObjectBox.init(this);
appContext = applicationContext
setIntKeyValue(appContext!!, RouteEngine.VALHALLA.ordinal, ROUTE_ENGINE)
navigationViewModel = getRouteEngine(appContext!!)
startKoin {
androidLogger(Level.DEBUG)
androidContext(this@MainApplication)
@@ -26,11 +38,12 @@ class MainApplication : Application() {
}
companion object {
var appContext: Context? = null
private set
var useContacts = false
val navigationViewModel = ViewModel(ValhallaRepository())
lateinit var navigationViewModel : ViewModel
}
}

View File

@@ -11,4 +11,5 @@ import org.koin.dsl.module
val appModule = module {
viewModelOf(::ViewModel)
singleOf(::ValhallaRepository)
singleOf(::OsrmRepository)
}

View File

@@ -48,7 +48,9 @@ import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
@@ -77,7 +79,7 @@ class MainActivity : ComponentActivity() {
var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute ->
if (newRoute.isNotEmpty()) {
routeModel.startNavigation(newRoute)
routeModel.startNavigation(newRoute, applicationContext)
routeData.value = routeModel.route.routeGeoJson
simulate()
//test()
@@ -295,12 +297,15 @@ class MainActivity : ComponentActivity() {
}
}
@OptIn(DelicateCoroutinesApi::class)
fun simulate() = GlobalScope.async {
for ((index, step) in routeModel.legs.steps.withIndex()) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
mock.setMockLocation(waypoint[1], waypoint[0])
delay(600L) //
fun simulate() {
CoroutineScope(Dispatchers.IO).launch {
for ((index, step) in routeModel.legs.steps.withIndex()) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
if (routeModel.isNavigating()) {
mock.setMockLocation(waypoint[1], waypoint[0])
delay(800L) //
}
}
}
}
}

View File

@@ -62,7 +62,13 @@ fun MapView(
Column {
NavigationInfo(step)
Box(contentAlignment = Alignment.Center) {
MapLibre(applicationContext, cameraState, baseStyle, route, ViewStyle.VIEW)
MapLibre(
applicationContext,
cameraState,
baseStyle,
route,
ViewStyle.VIEW
)
LocationTrackingEffect(
locationState = userLocationState,
) {

View File

@@ -257,7 +257,7 @@ private fun RecentPlaces(
modifier = Modifier.size(24.dp, 24.dp),
)
ListItem(
headlineContent = { Text("${place.name} ${place.postalCode}") },
headlineContent = { Text("${place.street} ${place.postalCode} ${place.city}") },
modifier = Modifier
.clickable {
val toLocation = location(place.longitude, place.latitude)

View File

@@ -24,11 +24,15 @@ import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
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.getRouteEngine
class NavigationSession : Session(), NavigationScreen.Listener {
@@ -69,7 +73,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
}
val navigationViewModel = ViewModel(ValhallaRepository())
lateinit var navigationViewModel : ViewModel
init {
val lifecycle: Lifecycle = lifecycle
@@ -77,6 +81,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
override fun onCreateScreen(intent: Intent): Screen {
navigationViewModel = getRouteEngine(carContext)
routeModel = RouteCarModel()
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)

View File

@@ -34,6 +34,7 @@ import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.bearing
@@ -57,7 +58,7 @@ class SurfaceRenderer(
private val cameraPosition = MutableLiveData(
CameraPosition(
zoom = 15.0,
target = Position(latitude = 48.1857475, longitude = 11.5793627)
target = Position(latitude = homeLocation.latitude, longitude = homeLocation.longitude)
)
)
private var visibleArea = MutableLiveData(
@@ -68,6 +69,7 @@ class SurfaceRenderer(
var height = 0
var lastBearing = 0.0
val routeData = MutableLiveData("")
val speedCamerasData = MutableLiveData("")
val speed = MutableLiveData(0F)
lateinit var centerLocation: Location
var viewStyle = ViewStyle.VIEW
@@ -165,11 +167,11 @@ class SurfaceRenderer(
}
}
@Composable
fun MapView() {
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val speedCameras: String? by speedCamerasData.observeAsState()
val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt)
@@ -177,7 +179,7 @@ class SurfaceRenderer(
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
}
DarkMode(carContext, baseStyle)
MapLibre(carContext, cameraState, baseStyle, route, viewStyle)
MapLibre(carContext, cameraState, baseStyle, route, viewStyle, speedCameras)
ShowPosition(cameraState, position, paddingValues)
}
@@ -189,9 +191,15 @@ class SurfaceRenderer(
) {
val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
val currentSpeed: Float? by speed.observeAsState()
val currentSpeed: Float? by speed.observeAsState()
if (viewStyle == ViewStyle.VIEW) {
DrawNavigationImages(paddingValues, currentSpeed, routeModel.routeState.maxSpeed, width, height)
DrawNavigationImages(
paddingValues,
currentSpeed,
routeModel.routeState.maxSpeed,
width,
height
)
}
LaunchedEffect(position, viewStyle) {
cameraState.animateTo(
@@ -237,7 +245,11 @@ class SurfaceRenderer(
fun updateLocation(location: Location) {
synchronized(this) {
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
val bearing = bearing(lastLocation, location, cameraPosition.value!!.bearing)
val bearing = bearing(
lastLocation,
location,
cameraPosition.value!!.bearing
)
val zoom = if (viewStyle == ViewStyle.VIEW) {
calculateZoom(location.speed.toDouble())
} else {

View File

@@ -47,6 +47,7 @@ import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.FeatureCollection
import org.maplibre.spatialk.geojson.Point
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
@@ -244,8 +245,8 @@ private fun rememberLocationSource(locationState: Location): GeoJsonSource {
buildJsonObject {
put("accuracy", location.accuracy)
put("bearing", location.bearing)
//put("bearingAccuracy", location.bearingAccuracy)
//put("age", location.timestamp.elapsedNow().inWholeNanoseconds)
put("bearingAccuracy", location.hasBearingAccuracy())
put("age", location.time.absoluteValue)
},
)
)

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.map
import android.annotation.SuppressLint
import android.location.Location
import android.content.Context
import androidx.compose.foundation.Canvas
@@ -36,6 +35,7 @@ import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
@@ -89,7 +89,8 @@ fun MapLibre(
cameraState: CameraState,
baseStyle: MutableState<BaseStyle.Uri>,
route: String?,
viewStyle: ViewStyle
viewStyle: ViewStyle,
speedCameras: String? = ""
) {
MaplibreMap(
options = MapOptions(
@@ -108,11 +109,12 @@ fun MapLibre(
} else {
RouteLayer(route)
}
SpeedCameraLayer(speedCameras)
}
//val lastLocation = location(cameraState.position.target.longitude, cameraState.position.target.latitude)
//Puck(cameraState, lastLocation)
}
}
@Composable
fun RouteLayer(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) {
@@ -167,6 +169,28 @@ fun AmenityLayer(routeData: String?) {
}
}
@Composable
fun SpeedCameraLayer(speedCameras: String?) {
if (speedCameras != null && speedCameras.isNotEmpty()) {
val color = const(Color.DarkGray)
val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras))
SymbolLayer(
id = "speed-camera-layer",
source = cameraSource,
iconImage = image(painterResource(R.drawable.speed_camera_48px), drawAsSdf = true),
iconColor = color,
iconSize =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4f),
6 to const(0.7f),
7 to const(1.75f),
20 to const(3f),
),
)
}
}
@Composable
fun BuildingLayer(tiles: Source) {
Anchor.Replace("building-3d") {
@@ -188,7 +212,9 @@ fun DrawNavigationImages(
height: Int
) {
NavigationImage(padding, width, height)
CurrentSpeed(width, height, speed)
if (speed != null) {
CurrentSpeed(width, height, speed, maxSpeed)
}
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
MaxSpeed(width, height, maxSpeed)
}
@@ -211,7 +237,8 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int) {
painter = painterResource(id = R.drawable.navigation_48px),
"Navigation",
tint = color.copy(alpha = 0.7f),
modifier = Modifier.size(imageSize.dp, imageSize.dp)
modifier = Modifier
.size(imageSize.dp, imageSize.dp)
.scale(scaleX = 1f, scaleY = 0.7f),
)
}
@@ -221,7 +248,8 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int) {
private fun CurrentSpeed(
width: Int,
height: Int,
speed: Float?
curSpeed: Float,
maxSpeed: Int
) {
val radius = 32
Box(
@@ -234,7 +262,8 @@ private fun CurrentSpeed(
) {
val textMeasurerSpeed = rememberTextMeasurer()
val textMeasurerKm = rememberTextMeasurer()
val speed = (speed!! * 3.6).toInt().toString()
val speed = (curSpeed * 3.6).toInt().toString()
val kmh = "km/h"
val styleSpeed = TextStyle(
fontSize = 22.sp,
@@ -245,10 +274,10 @@ private fun CurrentSpeed(
fontSize = 12.sp,
color = Color.White,
)
val textLayoutSpeed = remember(speed) {
val textLayoutSpeed = remember(speed, maxSpeed) {
textMeasurerSpeed.measure(speed, styleSpeed)
}
val textLayoutKm = remember(kmh) {
val textLayoutKm = remember(kmh, maxSpeed) {
textMeasurerSpeed.measure(kmh, styleKm)
}
Canvas(modifier = Modifier.fillMaxSize()) {

View File

@@ -34,6 +34,7 @@ import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.location
import kotlin.math.absoluteValue
@@ -60,7 +61,7 @@ class NavigationScreen(
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route)
routeModel.startNavigation(route, carContext)
surfaceRenderer.setRouteData()
invalidate()
}
@@ -92,6 +93,16 @@ class NavigationScreen(
var speedCameras = listOf<Elements>()
val speedObserver = Observer<List<Elements>> { cameras ->
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
val loc = location(0.0, 0.0)
cameras.forEach {
val loc =
location(longitude = it.lon!!, latitude = it.lat!!)
coordinates.add(listOf(it.lon!!, it.lat!!))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCamerasData.value =speedData
}
init {
@@ -339,7 +350,7 @@ class NavigationScreen(
private fun settingsAction(): Action {
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_applications_48px))
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext))
}
@@ -472,7 +483,7 @@ class NavigationScreen(
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
viewModel.getSpeedCameras(location)
viewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)

View File

@@ -54,13 +54,13 @@ class PlaceListScreen(
}
init {
if (category == Constants.RECENT) {
if (category == RECENT) {
viewModel.places.observe(this, observer)
}
if (category == Constants.CONTACTS) {
if (category == CONTACTS) {
viewModel.contactAddress.observe(this, observerAddress)
}
if (category == Constants.FAVORITES) {
if (category == FAVORITES) {
viewModel.favorites.observe(this, observer)
}
loadPlaces()
@@ -84,7 +84,7 @@ class PlaceListScreen(
places.forEach {
val row = Row.Builder()
.setImage(contactIcon(it.avatar, it.category))
.setTitle(it.name!!)
.setTitle("${it.street!!} ${it.city}")
.setOnClickListener {
val place = Place(
0,
@@ -152,7 +152,7 @@ class PlaceListScreen(
.setIcon(
RouteCarModel().createCarIcon(
carContext,
R.drawable.ic_pan_24
R.drawable.ic_close_white_24dp
)
)
.setOnClickListener {
@@ -167,7 +167,7 @@ class PlaceListScreen(
.build()
fun contactIcon(avatar: Uri?, category: String?): CarIcon {
if (category == Constants.RECENT || avatar == null) {
if (category == RECENT || avatar == null) {
return CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.ic_place_white_24dp

View File

@@ -52,7 +52,7 @@ class RoutePreviewScreen(
val navigationMessage = NavigationMessage(carContext)
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
routeModel.startNavigation(route)
routeModel.startNavigation(route, carContext)
surfaceRenderer.setPreviewRouteData(routeModel)
invalidate()
}

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M190,840L160,810L480,80L800,810L770,840L480,708L190,840Z"/>
</vector>

View File

@@ -8,6 +8,6 @@ val RouteColor = Color(0xFF5582D0)
val SpeedColor = Color(0xFF262525)
val MaxSpeedColor = Color(0xFF262525)
val MaxSpeedColor = Color(0xFFB71515)
val PlaceColor = Color(0xFF868005)

View File

@@ -124,9 +124,12 @@ data class BoundingBox (
object Constants {
const val STYLE: String = "https://kouros-online.de/liberty.json"
const val STYLE_DARK: String = "https://kouros-online.de/liberty_night.json"
//const val STYLE: String = "https://tiles.openfreemap.org/styles/liberty"
//const val STYLE: String = "https://kouros-online.de/liberty.json"
//const val STYLE_DARK: String = "https://kouros-online.de/liberty_night.json"
const val STYLE: String = "https://tiles.openfreemap.org/styles/liberty"
const val STYLE_DARK: String = "https://tiles.openfreemap.org/styles/liberty"
const val TAG: String = "Navigation"
const val CATEGORIES: String = "Categories"
@@ -176,6 +179,7 @@ object Constants {
const val DESTINATION_ARRIVAL_DISTANCE = 40.0
val ROUTE_ENGINE = RouteEngine.VALHALLA.name
}

View File

@@ -16,6 +16,7 @@
package com.kouros.navigation.data
import android.content.Context
import android.location.Location
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.RouteModel
@@ -36,10 +37,10 @@ abstract class NavigationRepository {
abstract fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String
fun getRouteDistance(currentLocation: Location, location: Location, searchFilter: SearchFilter): Double {
fun getRouteDistance(currentLocation: Location, location: Location, searchFilter: SearchFilter, context: Context): Double {
val route = getRoute(currentLocation, location, searchFilter)
val routeModel = RouteModel()
routeModel.startNavigation(route)
routeModel.startNavigation(route, context)
return routeModel.route.summary!!.distance
}

View File

@@ -2,6 +2,7 @@ package com.kouros.navigation.data
import android.location.Location
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.OsrmRoute
import com.kouros.navigation.data.route.Leg
@@ -10,6 +11,8 @@ import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.data.valhalla.ValhallaResponse
import com.kouros.navigation.data.valhalla.ValhallaRoute
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@@ -23,39 +26,51 @@ data class Route(
val summary: Summary?,
val legs: List<Leg>?,
val routeGeoJson: String = "",
val centerLocation : Location = location(0.0, 0.0),
var currentStep : Int = 0,
val centerLocation: Location = location(0.0, 0.0),
var currentStep: Int = 0,
val waypoints: List<List<Double>>?,
) {
) {
data class Builder (
data class Builder(
var routeEngine : Int = RouteEngine.VALHALLA.ordinal,
var routeEngine: Int = 0,
var summary: Summary? = null,
var legs: List<Leg>? = null,
var routeGeoJson: String = "",
var centerLocation: Location = location(0.0, 0.0),
var waypoints : List<List<Double>>? = null,) {
var waypoints: List<List<Double>>? = null,
) {
fun routeType (routeEngine: Int) = apply {this.routeEngine = routeEngine }
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun summary(summary: Summary) = apply { this.summary = summary }
fun legs(legs: List<Leg>) = apply { this.legs = legs }
fun routeGeoJson(routeGeoJson: String) = apply {
this.routeGeoJson = routeGeoJson
centerLocation = createCenterLocation(routeGeoJson)
}
fun waypoints(waypoints: List<List<Double>>) = apply { this.waypoints = waypoints }
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun waypoints(waypoints: List<List<Double>>) = apply { this.waypoints = waypoints }
fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create()
if (routeEngine == RouteEngine.VALHALLA.ordinal) {
val jsonObject: Map<String, JsonElement> = Json.parseToJsonElement(route).jsonObject
val routeJson = gson.fromJson(jsonObject["trip"].toString(), ValhallaResponse::class.java)
when (this.routeEngine) {
RouteEngine.VALHALLA.ordinal -> {
val jsonObject: Map<String, JsonElement> =
Json.parseToJsonElement(route).jsonObject
val routeJson =
gson.fromJson(
jsonObject["trip"].toString(),
ValhallaResponse::class.java
)
ValhallaRoute().mapJsonToValhalla(routeJson, this)
} else {
}
else -> {
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
OsrmRoute().mapToOsrm(osrmJson, this)
}
}
}
}

View File

@@ -4,7 +4,7 @@ import android.location.Location
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
private const val routeUrl = "https://router.project-osrm.org/route/v1/driving/"
private const val routeUrl = "https://kouros-online.de/osrm/route/v1/driving/"
class OsrmRepository : NavigationRepository() {
override fun getRoute(
@@ -12,7 +12,7 @@ class OsrmRepository : NavigationRepository() {
location: Location,
searchFilter: SearchFilter
): String {
val routeLocation = "${currentLocation.latitude},${currentLocation.longitude};${location.latitude},${location.longitude}?steps=true"
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true"
return fetchUrl(routeUrl + routeLocation, true)
}
}

View File

@@ -1,11 +1,84 @@
package com.kouros.navigation.data.osrm
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.valhalla.ValhallaResponse
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline
class OsrmRoute {
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) {
val waypoints = mutableListOf<List<Double>>()
val summary = Summary()
summary.distance = routeJson.routes.first().distance!!
summary.duration = routeJson.routes.first().duration!!
val steps = mutableListOf<Step>()
var stepIndex = 0
routeJson.routes.first().legs.first().steps.forEach {
if (it.maneuver != null) {
val points = decodePolyline(it.geometry!!, 5)
waypoints.addAll(points)
val maneuver = RouteManeuver(
bearingBefore = it.maneuver!!.bearingBefore ?: 0,
bearingAfter = it.maneuver!!.bearingAfter ?: 0,
type = convertType(it.maneuver!!),
waypoints = points
)
val step = Step( index = stepIndex, name = it.name!!, distance = it.distance!!, duration = it.duration!!, maneuver = maneuver)
steps.add(step)
stepIndex += 1
}
}
val leg = Leg(steps)
builder
.routeType(1)
.summary(summary)
.routeGeoJson(createLineStringCollection(waypoints))
.legs(listOf(leg))
.waypoints(waypoints.toList())
}
fun convertType(maneuver: Maneuver): Int {
var newType = 0
when (maneuver.type) {
ManeuverType.depart.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
}
ManeuverType.arrive.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
}
ManeuverType.continue_.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
}
ManeuverType.turn.value -> {
if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
}
}
}
return newType
}
}
enum class ManeuverType(val value: String) {
turn("turn"),
depart("depart"),
arrive("arrive"),
merge("merge"),
onRamp("on ramp"),
offRamp("off ramp"),
fork("fork"),
endOfRoad("end of road"),
continue_("continue"),
roundAbout("roundabout"),
rotary("rotary"),
roundaboutTurn("roundabout turn"),
notification("notification"),
exitRoundabout("exit roundabout"),
exitRotary("exit rotary")
}

View File

@@ -10,7 +10,10 @@ import java.net.URL
class Overpass {
val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
//val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
fun getAround(radius: Int, linestring: String) : List<Elements> {
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
@@ -28,7 +31,7 @@ class Overpass {
|);
|out body;
""".trimMargin()
println("way[highway](around:$radius,$linestring)")
//println("way[highway](around:$radius,$linestring)")
return overpassApi(httpURLConnection, searchQuery)
}
@@ -71,7 +74,7 @@ class Overpass {
.use { it.readText() } // defaults to UTF-8
val gson = GsonBuilder().serializeNulls().create()
val overpass = gson.fromJson(response, Amenity::class.java)
//println("Overpass: $response")
// println("Overpass: $response")
return overpass.elements
}
return emptyList()

View File

@@ -245,6 +245,7 @@
"type": "fill",
"source": "openmaptiles",
"source-layer": "water",
"maxzoom": 24,
"filter": ["!=", ["get", "brunnel"], "tunnel"],
"paint": {"fill-color": "rgb(158,189,255)"}
},
@@ -860,6 +861,7 @@
true,
false
],
"layout": {"visibility": "visible"},
"paint": {"fill-pattern": "pedestrian_polygon"}
},
{
@@ -1376,11 +1378,13 @@
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"maxzoom": 24,
"filter": [
"all",
["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true],
["==", ["get", "class"], "transit"]
],
"layout": {"visibility": "visible"},
"paint": {
"line-color": "#bbb",
"line-width": [
@@ -2201,7 +2205,8 @@
"text-font": ["Noto Sans Italic"],
"text-max-width": 9,
"text-offset": [0, 0.6],
"text-size": 12
"text-size": 12,
"visibility": "none"
},
"paint": {
"text-color": "#666",
@@ -2240,7 +2245,8 @@
"text-font": ["Noto Sans Italic"],
"text-max-width": 9,
"text-offset": [0, 0.6],
"text-size": 12
"text-size": 12,
"visibility": "none"
},
"paint": {
"text-color": "#666",
@@ -2279,7 +2285,8 @@
"text-font": ["Noto Sans Italic"],
"text-max-width": 9,
"text-offset": [0, 0.6],
"text-size": 12
"text-size": 12,
"visibility": "none"
},
"paint": {
"text-color": "#666",
@@ -2313,7 +2320,8 @@
"text-font": ["Noto Sans Italic"],
"text-max-width": 9,
"text-offset": [0.9, 0],
"text-size": 12
"text-size": 12,
"visibility": "none"
},
"paint": {
"text-color": "#2e5a80",

View File

@@ -137,7 +137,7 @@
"source": "openmaptiles",
"source-layer": "landuse",
"filter": ["==", ["get", "class"], "pitch"],
"paint": {"fill-color": "#DEE3CD"}
"paint": {"fill-color": "rgba(49, 49, 40, 1)"}
},
{
"id": "landuse_track",
@@ -247,7 +247,7 @@
"source": "openmaptiles",
"source-layer": "water",
"filter": ["!=", ["get", "brunnel"], "tunnel"],
"paint": {"fill-color": "rgb(158,189,255)"}
"paint": {"fill-color": "rgba(34, 54, 98, 1)"}
},
{
"id": "landcover_sand",
@@ -255,7 +255,7 @@
"source": "openmaptiles",
"source-layer": "landcover",
"filter": ["==", ["get", "class"], "sand"],
"paint": {"fill-color": "rgba(247, 239, 195, 1)"}
"paint": {"fill-color": "rgba(148, 146, 138, 1)"}
},
{
"id": "aeroway_fill",
@@ -654,7 +654,7 @@
],
"layout": {"line-join": "round"},
"paint": {
"line-color": "#fff",
"line-color": "rgba(74, 64, 64, 1)",
"line-width": [
"interpolate",
["exponential", 1.2],
@@ -706,7 +706,7 @@
],
"layout": {"line-join": "round"},
"paint": {
"line-color": "#fff4c6",
"line-color": "rgba(72, 70, 58, 1)",
"line-width": [
"interpolate",
["exponential", 1.2],
@@ -1154,7 +1154,7 @@
],
"layout": {"line-cap": "round", "line-join": "round"},
"paint": {
"line-color": "#fff",
"line-color": "rgba(55, 53, 53, 1)",
"line-width": [
"interpolate",
["exponential", 1.2],
@@ -1691,7 +1691,7 @@
["match", ["get", "class"], ["path", "pedestrian"], true, false]
],
"paint": {
"line-color": "hsl(0,0%,100%)",
"line-color": "rgba(107, 102, 102, 1)",
"line-dasharray": [1, 0.3],
"line-width": [
"interpolate",
@@ -2208,7 +2208,8 @@
"text-font": ["Noto Sans Italic"],
"text-max-width": 9,
"text-offset": [0, 0.6],
"text-size": 12
"text-size": 12,
"visibility": "none"
},
"paint": {
"text-color": "#666",
@@ -2247,7 +2248,8 @@
"text-font": ["Noto Sans Italic"],
"text-max-width": 9,
"text-offset": [0, 0.6],
"text-size": 12
"text-size": 12,
"visibility": "none"
},
"paint": {
"text-color": "#666",
@@ -2286,7 +2288,8 @@
"text-font": ["Noto Sans Italic"],
"text-max-width": 9,
"text-offset": [0, 0.6],
"text-size": 12
"text-size": 12,
"visibility": "none"
},
"paint": {
"text-color": "#666",
@@ -2320,7 +2323,8 @@
"text-font": ["Noto Sans Italic"],
"text-max-width": 9,
"text-offset": [0.9, 0],
"text-size": 12
"text-size": 12,
"visibility": "none"
},
"paint": {
"text-color": "#2e5a80",

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.data
package com.kouros.navigation.data.valhalla
enum class ManeuverType(val value: Int) {
None(0),

View File

@@ -8,6 +8,8 @@ import com.kouros.navigation.data.ValhallaLocation
import kotlinx.serialization.json.Json
private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
class ValhallaRepository : NavigationRepository() {
override fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {

View File

@@ -1,20 +1,28 @@
package com.kouros.navigation.model
import android.content.Context
import android.location.Location
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step
import com.kouros.data.R
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.ManeuverType
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.valhalla.ManeuverType
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
@@ -43,8 +51,10 @@ open class RouteModel() {
val legs: Leg
get() = routeState.route!!.legs!!.first()
fun startNavigation(routeString: String) {
fun startNavigation(routeString: String, context: Context) {
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE)
val newRoute = Route.Builder()
.routeEngine(routeEngine)
.route(routeString)
.build()
this.routeState = routeState.copy(
@@ -65,15 +75,13 @@ open class RouteModel() {
@OptIn(DelicateCoroutinesApi::class)
fun updateLocation(location: Location, viewModel: ViewModel) {
findStep(location)
GlobalScope.launch(Dispatchers.IO) {
updateSpeedLimit(location, viewModel)
}
updateSpeedLimit(location, viewModel)
}
private fun findStep(location: Location) {
var nearestDistance = 100000.0f
for ((index, step) in legs.steps.withIndex()) {
if (index >= route.currentStep && nearestDistance > 0) {
if (index >= route.currentStep) {
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
if (wayIndex >= step.waypointIndex) {
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
@@ -83,22 +91,31 @@ open class RouteModel() {
step.waypointIndex = wayIndex
}
}
if (nearestDistance == 0F) {
break
}
}
}
if (nearestDistance == 0F) {
break
}
}
//println("Current Index ${route.currentStep} WayPoint: ${route.currentStep().waypointIndex}")
}
fun updateSpeedLimit(location: Location, viewModel: ViewModel) {
// speed limit
val distance = routeState.lastSpeedLocation.distanceTo(location)
if (distance > 500 || routeState.lastSpeedIndex < route.currentStep) {
routeState = routeState.copy(lastSpeedIndex = route.currentStep)
routeState = routeState.copy(lastSpeedLocation = location)
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
if (it.tags.name != null && it.tags.maxspeed != null) {
val speed = it.tags.maxspeed!!.toInt()
routeState = routeState.copy(maxSpeed = speed)
fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking {
CoroutineScope(Dispatchers.IO).launch {
// speed limit
val distance = routeState.lastSpeedLocation.distanceTo(location)
if (distance > 500 || routeState.lastSpeedIndex < route.currentStep) {
routeState = routeState.copy(lastSpeedIndex = route.currentStep)
routeState = routeState.copy(lastSpeedLocation = location)
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
if (it.tags.name != null && it.tags.maxspeed != null) {
val speed = it.tags.maxspeed!!.toInt()
routeState = routeState.copy(maxSpeed = speed)
}
}
}
}
@@ -148,11 +165,9 @@ open class RouteModel() {
val maneuverType = step.maneuver.type
val distanceLeft = leftStepDistance()
var text = ""
when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
}
else -> {
if (step.name.isNotEmpty()) {
text = step.name
@@ -174,11 +189,13 @@ open class RouteModel() {
fun travelLeftTime(): Double {
var timeLeft = 0.0
// time for next step until end step
for (i in route.currentStep + 1..<legs.steps.size) {
val step = legs.steps[i]
timeLeft += step.duration
}
val step = route.nextStep()
// time for current step
val step = route.currentStep()
val curTime = step.duration
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
@@ -220,8 +237,8 @@ open class RouteModel() {
val curDistance = step.distance
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
val time = curDistance * percent / 100
leftDistance += time
val distance = curDistance * percent / 100
leftDistance += distance
return leftDistance
}

View File

@@ -109,7 +109,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
repository.getRouteDistance(
location,
plLocation,
getSearchFilter(context)
getSearchFilter(context), context
)
place.distance = distance.toFloat()
}
@@ -134,7 +134,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context))
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context)
place.distance = distance.toFloat()
}
favorites.postValue(results)
@@ -265,9 +265,9 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun getSpeedCameras(location: Location) {
fun getSpeedCameras(location: Location, radius : Double) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location, 5.0)
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
val distAmenities = mutableListOf<Elements>()
amenities.forEach {
val plLocation =
@@ -380,7 +380,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context))
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context)
place.distance = distance.toFloat()
}
} catch (e: Exception) {

View File

@@ -10,6 +10,7 @@ import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.dsl.addFeature
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
import org.maplibre.spatialk.geojson.dsl.buildLineString
import org.maplibre.spatialk.geojson.dsl.buildMultiPoint
import org.maplibre.spatialk.geojson.toJson
import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc
@@ -32,8 +33,9 @@ object GeoUtils {
return newLocation
}
fun decodePolyline(encoded: String, vararg precisionOptional: Int): List<List<Double>> {
val precision = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6
fun decodePolyline(encoded: String, precision: Int = 6,): List<List<Double>> {
//val precisionOptional = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6
val factor = 10.0.pow(precision)
var lat = 0

View File

@@ -4,7 +4,12 @@ import android.content.Context
import android.location.Location
import android.location.LocationManager
import androidx.core.content.edit
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
@@ -20,6 +25,14 @@ import kotlin.time.Duration.Companion.seconds
object NavigationUtils {
fun getRouteEngine(context: Context): ViewModel {
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE)
return when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
else -> ViewModel(OsrmRepository())
}
}
fun getBooleanKeyValue(context: Context, key: String): Boolean {
return context
.getSharedPreferences(
@@ -43,13 +56,13 @@ object NavigationUtils {
}
}
fun getIntKeyValue(context: Context, key: String): Int {
fun getIntKeyValue(context: Context, key: String, default: Int = 0): Int {
return context
.getSharedPreferences(
SHARED_PREF_KEY,
Context.MODE_PRIVATE
)
.getInt(key, 0)
.getInt(key, default)
}
fun setIntKeyValue(context: Context, `val`: Int, key: String) {

View File

@@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M190,840L160,810L480,80L800,810L770,840L480,708L190,840ZM258,742L480,644L702,742L480,228L258,742ZM480,644L480,644L480,644L480,644Z"/>
android:pathData="M190,840L160,810L480,80L800,810L770,840L480,708L190,840Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M388,880L368,754Q349,747 328,735Q307,723 291,710L173,764L80,600L188,521Q186,512 185.5,500.5Q185,489 185,480Q185,471 185.5,459.5Q186,448 188,439L80,360L173,196L291,250Q307,237 328,225Q349,213 368,207L388,80L572,80L592,206Q611,213 632.5,224.5Q654,236 669,250L787,196L880,360L772,437Q774,447 774.5,458.5Q775,470 775,480Q775,490 774.5,501Q774,512 772,522L880,600L787,764L669,710Q653,723 632.5,735.5Q612,748 592,754L572,880L388,880ZM436,820L524,820L538,708Q571,700 600.5,683Q630,666 654,642L760,688L800,616L706,547Q710,530 712.5,513.5Q715,497 715,480Q715,463 713,446.5Q711,430 706,413L800,344L760,272L654,318Q631,292 602,274.5Q573,257 538,252L524,140L436,140L422,252Q388,259 358.5,276Q329,293 306,318L200,272L160,344L254,413Q250,430 247.5,446.5Q245,463 245,480Q245,497 247.5,513.5Q250,530 254,547L160,616L200,688L306,642Q330,666 359.5,683Q389,700 422,708L436,820ZM480,610Q534,610 572,572Q610,534 610,480Q610,426 572,388Q534,350 480,350Q426,350 388,388Q350,426 350,480Q350,534 388,572Q426,610 480,610ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M452,674L508,674L518,620Q538,614 552,605Q566,596 578,584L640,603L666,549L619,519Q623,498 623,480Q623,462 619,441L666,411L640,357L578,376Q566,364 552,355Q538,346 518,340L508,286L452,286L442,340Q422,346 408,355Q394,364 382,376L320,357L294,411L341,441Q337,462 337,480Q337,498 341,519L294,549L320,603L382,584Q394,596 408,605Q422,614 442,620L452,674ZM480,565Q444,565 419.5,540.5Q395,516 395,480Q395,444 419.5,419.5Q444,395 480,395Q516,395 540.5,419.5Q565,444 565,480Q565,516 540.5,540.5Q516,565 480,565ZM180,840Q156,840 138,822Q120,804 120,780L120,180Q120,156 138,138Q156,120 180,120L780,120Q804,120 822,138Q840,156 840,180L840,780Q840,804 822,822Q804,840 780,840L180,840ZM180,780L780,780Q780,780 780,780Q780,780 780,780L780,180Q780,180 780,180Q780,180 780,180L180,180Q180,180 180,180Q180,180 180,180L180,780Q180,780 180,780Q180,780 180,780ZM180,180L180,180Q180,180 180,180Q180,180 180,180L180,780Q180,780 180,780Q180,780 180,780L180,780Q180,780 180,780Q180,780 180,780L180,180Q180,180 180,180Q180,180 180,180Z"/>
</vector>