Refactoring

This commit is contained in:
Dimitris
2025-12-12 15:32:15 +01:00
parent a02673af36
commit 72872cddeb
21 changed files with 588 additions and 349 deletions

View File

@@ -162,7 +162,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snapedLocation)
if (distance > MAXIMAL_ROUTE_DEVIATION) {
navigationScreen.calculateNewRoute(routeModel.destination)
navigationScreen.calculateNewRoute(routeModel.routeState.destination)
return
}
navigationScreen.updateTrip(location)

View File

@@ -5,14 +5,14 @@ import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.location.Location
import android.location.LocationManager
import android.os.CountDownTimer
import android.os.Handler
import android.util.Log
import androidx.car.app.AppManager
import androidx.car.app.CarContext
import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer
import androidx.car.app.connection.CarConnection
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -27,31 +27,24 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.map.DarkMode
import com.kouros.navigation.car.map.DrawImage
import com.kouros.navigation.car.map.RouteLayer
import com.kouros.navigation.car.map.MapLibre
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.SHOW_THREED_BUILDING
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.duration
import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.previewZoom
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.math.absoluteValue
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
class SurfaceRenderer(
@@ -59,14 +52,14 @@ class SurfaceRenderer(
private var routeModel: RouteCarModel
) : DefaultLifecycleObserver {
var lastLocation = Location(LocationManager.GPS_PROVIDER)
val cameraPosition = MutableLiveData(
var lastLocation = location(0.0, 0.0)
private val cameraPosition = MutableLiveData(
CameraPosition(
zoom = 15.0,
target = Position(latitude = 48.1857475, longitude = 11.5793627)
)
)
var visibleArea = MutableLiveData(
private var visibleArea = MutableLiveData(
Rect(0, 0, 0, 0)
)
@@ -81,6 +74,7 @@ class SurfaceRenderer(
val previewRouteData = MutableLiveData("")
val speed = MutableLiveData(0F)
lateinit var centerLocation: Location
var preview = false
@@ -90,6 +84,8 @@ class SurfaceRenderer(
var tilt = 55.0
var previewDistance = 0.0
var countDownTimerActive = false
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
lateinit var lifecycleOwner: CustomLifecycleOwner
@@ -171,6 +167,7 @@ class SurfaceRenderer(
init {
lifecycle.addObserver(this)
speed.value = 0F
}
fun onConnectionStateUpdated(connectionState: Int) {
@@ -192,26 +189,8 @@ class SurfaceRenderer(
val baseStyle = remember {
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
}
baseStyle.value =
(if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
Constants.STYLE
))
MaplibreMap(
options = MapOptions(
ornamentOptions =
OrnamentOptions(isScaleBarEnabled = false)),
cameraState = cameraState,
baseStyle = baseStyle.value
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = carContext, SHOW_THREED_BUILDING)) {
BuildingLayer(tiles)
}
RouteLayer(route, previewRoute, position!!.zoom)
}
//Puck(cameraState, lastLocation)
}
DarkMode(carContext, baseStyle)
MapLibre(carContext, cameraState, baseStyle, route, previewRoute, position)
ShowPosition(cameraState, position, paddingValues)
}
@@ -221,16 +200,17 @@ class SurfaceRenderer(
position: CameraPosition?,
paddingValues: PaddingValues
) {
val cameraDuration = duration(position)
var bearing = position!!.bearing
val cameraDuration = duration(preview, position!!.bearing, lastBearing)
var bearing = position.bearing
var zoom = position.zoom
var target = position.target
var localTilt = tilt
val currentSpeed: Float? by speed.observeAsState()
if (!preview) {
if (routeModel.isNavigating()) {
DrawImage(paddingValues, lastLocation, width, height, "")
DrawImage(paddingValues, currentSpeed, width, height)
} else {
DrawImage(paddingValues, lastLocation, width, height, "")
DrawImage(paddingValues, currentSpeed, width, height)
}
} else {
bearing = 0.0
@@ -259,18 +239,6 @@ class SurfaceRenderer(
.setSurfaceCallback(mSurfaceCallback)
}
private fun duration(position: CameraPosition?): Duration {
if (preview) {
return 3.seconds
}
val cameraDuration = if ((lastBearing - position!!.bearing).absoluteValue > 20.0) {
2.seconds
} else {
1.seconds
}
return cameraDuration
}
/** Handles the map zoom-in and zoom-out events. */
fun handleScale(zoomSign: Int) {
synchronized(this) {
@@ -313,6 +281,13 @@ class SurfaceRenderer(
)
lastBearing = cameraPosition.value!!.bearing
lastLocation = location
speed.value = location.speed
if (!countDownTimerActive) {
countDownTimerActive = true
val mainThreadHandler = Handler(carContext.mainLooper)
val lastLocationTimer = lastLocation
checkUpdate(mainThreadHandler, lastLocationTimer)
}
} else {
updateCameraPosition(
0.0,
@@ -323,6 +298,23 @@ class SurfaceRenderer(
}
}
private fun checkUpdate(
mainThreadHandler: Handler,
lastLocationTimer: Location
) {
mainThreadHandler.post {
object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
countDownTimerActive = false
if (lastLocation.time - lastLocationTimer.time < 1500) {
speed.postValue(0F)
}
}
}.start()
}
}
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
cameraPosition.postValue(
cameraPosition.value!!.copy(
@@ -344,7 +336,7 @@ class SurfaceRenderer(
fun setPreviewRouteData(routeModel: RouteModel) {
previewRouteData.value = routeModel.route.routeGeoJson
centerLocation = routeModel.centerLocation
centerLocation = routeModel.route.centerLocation
preview = true
previewDistance = routeModel.route.distance
}

View File

@@ -1,7 +1,9 @@
package com.kouros.navigation.car.map
import android.location.Location
import android.content.Context
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@@ -12,6 +14,7 @@ import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -25,9 +28,13 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.NavigationColor
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 org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
@@ -39,9 +46,14 @@ import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.GeoJsonData
import org.maplibre.compose.sources.Source
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
@@ -65,6 +77,32 @@ fun cameraState(
)
}
@Composable
fun MapLibre(
context: Context,
cameraState: CameraState,
baseStyle: MutableState<BaseStyle.Uri>,
route: String?,
previewRoute: String?,
position: CameraPosition?
) {
MaplibreMap(
options = MapOptions(
ornamentOptions =
OrnamentOptions(isScaleBarEnabled = false)
),
cameraState = cameraState,
baseStyle = baseStyle.value
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
BuildingLayer(tiles)
}
RouteLayer(route, previewRoute, position!!.zoom)
}
//Puck(cameraState, lastLocation)
}
}
@Composable
fun RouteLayer(routeData: String?, previewRoute: String?, zoom: Double) {
val width = zoom - 2
@@ -115,26 +153,20 @@ fun BuildingLayer(tiles: Source) {
}
@Composable
fun DrawImage(padding: PaddingValues, location: Location, width: Int, height: Int, street: String) {
NavigationImage(padding, width,height, street)
Speed(width, height, location)
fun DrawImage(padding: PaddingValues, speed: Float?, width: Int, height: Int) {
NavigationImage(padding, width,height)
Speed(width, height, speed)
}
@Composable
fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: String) {
fun NavigationImage(padding: PaddingValues, width: Int, height: Int) {
val imageSize = (height/6)
val color = remember { NavigationColor }
BadgedBox(
modifier = Modifier
.padding(padding),
badge = {
Badge()
}
) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
Canvas(modifier =Modifier
.size(imageSize.dp, imageSize.dp)) {
scale(scaleX = 1f, scaleY = 0.7f) {
drawCircle(Color.DarkGray.copy(alpha = 0.2f))
drawCircle(Color.DarkGray.copy(alpha = 0.4f))
}
}
Icon(
@@ -143,8 +175,6 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: Str
tint = color.copy(alpha = 1f),
modifier = Modifier.size(imageSize.dp, imageSize.dp),
)
if (street.isNotEmpty())
Text(text = street)
}
}
@@ -152,7 +182,7 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: Str
private fun Speed(
width: Int,
height: Int,
location: Location
speed: Float?
) {
val radius = 32
Box(
@@ -165,7 +195,7 @@ private fun Speed(
) {
val textMeasurerSpeed = rememberTextMeasurer()
val textMeasurerKm = rememberTextMeasurer()
val speed = (location.speed * 3.6).toInt().toString()
val speed = (speed!! * 3.6).toInt().toString()
val kmh = "km/h"
val styleSpeed = TextStyle(
fontSize = 22.sp,
@@ -212,6 +242,23 @@ private fun Speed(
}
}
@Composable
fun DarkMode(context: Context, baseStyle: MutableState<BaseStyle.Uri>) {
val darkMode = getIntKeyValue(context, Constants.DARK_MODE_SETTINGS)
if (darkMode == 0) {
baseStyle.value = BaseStyle.Uri(Constants.STYLE)
}
if (darkMode == 1) {
baseStyle.value = BaseStyle.Uri(Constants.STYLE_DARK)
}
if (darkMode == 2) {
baseStyle.value =
(if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
Constants.STYLE
))
}
}
fun getPaddingValues(width: Int, height: Int, preView: Boolean): PaddingValues {
return if (preView) {
PaddingValues(start = 150.dp, bottom = 0.dp)

View File

@@ -49,7 +49,7 @@ class RouteCarModel() : RouteModel() {
.setIcon(createCarIcon(carContext, stepData.icon))
.build()
)
.setRoad(destination.street!!)
.setRoad(routeState.destination.street!!)
.build()
return step
}

View File

@@ -0,0 +1,87 @@
package com.kouros.navigation.car.screen
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import com.kouros.data.R
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
class DarkModeSettings(private val carContext: CarContext) : Screen(carContext) {
private var darkModeSettings = 0
init {
darkModeSettings = getIntKeyValue(carContext, DARK_MODE_SETTINGS)
}
override fun onGetTemplate(): Template {
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
.addItem(
buildRowForTemplate(
R.string.off_action_title,
)
)
.addItem(
buildRowForTemplate(
R.string.on_action_title,
)
)
.addItem(
buildRowForTemplate(
R.string.use_telephon_settings,
)
)
.setOnSelectedListener { index: Int ->
this.onSelected(index)
}
.setSelectedIndex(darkModeSettings)
.build()
return templateBuilder
.addSectionedList(SectionedItemList.create(
radioList,
carContext.getString(R.string.dark_mode)
))
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.dark_mode))
.setStartHeaderAction(Action.BACK)
.build()
)
.build()
}
private fun onSelected(index: Int) {
setIntKeyValue(carContext, index, DARK_MODE_SETTINGS)
CarToast.makeText(
carContext,
(carContext
.getString(R.string.display_settings)
+ ":"
+ " " + index), CarToast.LENGTH_LONG
)
.show()
}
private fun buildRowForTemplate(title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.build()
}
}

View File

@@ -1,12 +1,12 @@
package com.kouros.navigation.car.screen
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.OnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
@@ -19,6 +19,7 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
private var buildingToggleState = false
init {
buildingToggleState = getBooleanKeyValue(carContext, SHOW_THREED_BUILDING)
}
@@ -35,7 +36,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
buildingToggleState = !buildingToggleState
}.setChecked(buildingToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle))
listBuilder.addItem(
buildRowForScreenTemplate(
DarkModeSettings(carContext),
R.string.dark_mode
)
)
return ListTemplate.Builder()
.setSingleList(listBuilder.build())
.setHeader(
@@ -54,4 +60,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
.setToggle(toggle)
.build()
}
private fun buildRowForScreenTemplate(screen: Screen, title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setOnClickListener { screenManager.push(screen) }
.setBrowsable(true)
.build()
}
}

View File

@@ -111,11 +111,11 @@ class NavigationScreen(
}
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.isArrived()) {
if (routeModel.routeState.arrived) {
val timer = object : CountDownTimer(10000, 10000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
routeModel.arrived = false
routeModel.routeState = routeModel.routeState.copy(arrived = false)
invalidate()
}
}
@@ -135,12 +135,16 @@ class NavigationScreen(
}
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = ""
if (routeModel.routeState.destination.street != null) {
street = routeModel.routeState.destination.street!!
}
return NavigationTemplate.Builder()
.setNavigationInfo(
MessageInfo.Builder(
carContext.getString(R.string.arrived_exclamation_msg)
)
.setText(routeModel.destination.street!!)
.setText(street)
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
@@ -192,7 +196,7 @@ class NavigationScreen(
}
fun getRoutingInfo(): RoutingInfo {
var currentDistance = routeModel.currentDistance
var currentDistance = routeModel.leftStepDistance()
val displayUnit = if (currentDistance > 1000.0) {
currentDistance /= 1000.0
Distance.UNIT_KILOMETERS
@@ -274,7 +278,7 @@ class NavigationScreen(
.setOnClickListener {
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo)
routeModel.destination = recentPlace
routeModel.routeState.destination = recentPlace
}
.build()
}
@@ -398,7 +402,7 @@ class NavigationScreen(
viewModel.saveRecent(place)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
currentNavigationLocation = location
routeModel.destination = place
routeModel.routeState.destination = place
invalidate()
}
}
@@ -416,7 +420,7 @@ class NavigationScreen(
invalidate()
val mainThreadHandler = Handler(carContext.mainLooper)
mainThreadHandler.post {
object : CountDownTimer(5000, 1000) {
object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
calculateNewRoute = false
@@ -427,19 +431,20 @@ class NavigationScreen(
}
fun reRoute(destination: Place) {
val dest = location( destination.longitude, destination.latitude)
val dest = location(destination.longitude, destination.latitude)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, dest)
}
fun updateTrip(location: Location) {
val start = System.currentTimeMillis()
routeModel.updateLocation(location)
val end = System.currentTimeMillis()
println("Time ${end-start}")
if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION
&& routeModel.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) {
routeModel.arrived = true
routeModel.stopNavigation()
with(routeModel) {
updateLocation(location)
if (routeState.maneuverType == Maneuver.TYPE_DESTINATION
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
routeState = routeState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
}
}
invalidate()
}

View File

@@ -2,7 +2,7 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color
val NavigationColor = Color(0xFF052186)
val NavigationColor = Color(0xFF0730B2)
val RouteColor = Color(0xFF5582D0)

View File

@@ -164,6 +164,8 @@ object Constants {
const val SHOW_THREED_BUILDING = "Show3D"
const val DARK_MODE_SETTINGS = "DarkMode"
const val AVOID_MOTORWAY = "AvoidMotorway"
const val AVOID_TOLLWAY = "AvoidTollway"

View File

@@ -1,22 +1,28 @@
package com.kouros.navigation.data
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.valhalla.Maneuvers
import com.kouros.navigation.data.valhalla.Summary
import com.kouros.navigation.data.valhalla.Trip
import com.kouros.navigation.data.valhalla.ValhallaJson
import com.kouros.navigation.utils.NavigationUtils.createCenterLocation
import com.kouros.navigation.utils.NavigationUtils.createGeoJson
import com.kouros.navigation.utils.NavigationUtils.decodePolyline
import com.kouros.navigation.utils.location
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
import org.maplibre.turf.TurfMeasurement
import kotlin.math.roundToInt
data class Route (
data class Route(
/**
* A Leg is a route between only two waypoints.
*
* @since 1.0.0
*/
var maneuvers: List<Maneuvers>,
val maneuvers: List<Maneuvers>,
/**
* The distance traveled from origin to destination.
@@ -33,9 +39,16 @@ data class Route (
*
* @since 1.0.0
*/
var waypoints: List<List<Double>>,
val waypoints: List<List<Double>>,
val pointLocations : List<Point>,
/**
* List of [List<Point>] objects. Each `Point` is an input coordinate
* snapped to the road and path network. The `waypoint` appear in the list in the order of
* the input coordinates.
*
* @since 1.0.0
*/
val pointLocations: List<Point>,
val summary: Summary,
@@ -43,27 +56,26 @@ data class Route (
val time: Double,
var routeGeoJson : String,
val routeGeoJson: String,
var currentManeuverIndex: Int
val currentManeuverIndex : Int,
val centerLocation: Location
) {
class Builder {
private lateinit var maneuvers: List<Maneuvers>
private var distance: Double = 0.0
private var time: Double = 0.0
private lateinit var waypoints: List<List<Double>>
private lateinit var pointLocations: List<Point>
private lateinit var summary : Summary
private lateinit var trip : Trip
private lateinit var summary: Summary
private lateinit var trip: Trip
private var routeGeoJson = ""
private var centerLocation = location(0.0, 0.0)
fun route (route: String ) = apply {
fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create()
val valhalla = gson.fromJson(route, ValhallaJson::class.java)
@@ -83,35 +95,37 @@ data class Route (
points.add(point)
}
pointLocations = points
this.routeGeoJson = createGeoJson(waypoints)
routeGeoJson = createGeoJson(waypoints)
centerLocation = createCenterLocation(routeGeoJson)
return Route(
maneuvers, distance, waypoints, pointLocations, summary, trip, time, routeGeoJson, 0
maneuvers,
distance,
waypoints,
pointLocations,
summary,
trip,
time,
routeGeoJson,
0,
centerLocation
)
}
}
fun maneuverLocations(): List<Point> {
val beginShapeIndex = currentManeuver().beginShapeIndex
val endShapeIndex = if (currentManeuver().endShapeIndex >= waypoints.size) {
waypoints.size
} else {
currentManeuver().endShapeIndex + 1
}
//return pointLocations.subList(beginShapeIndex, endShapeIndex)
return pointLocations
}
fun clear() {
waypoints = mutableListOf()
maneuvers = mutableListOf()
routeGeoJson = ""
}
fun currentManeuver() : Maneuvers {
fun currentManeuver(): Maneuvers {
return maneuvers[currentManeuverIndex]
}
fun nextManeuver() : Maneuvers {
return maneuvers[currentManeuverIndex+1]
fun nextManeuver(): Maneuvers {
val nextIndex = currentManeuverIndex + 1
return if (nextIndex < maneuvers.size) {
maneuvers[nextIndex]
} else {
throw IndexOutOfBoundsException("No next maneuver available.")
}
}
}

View File

@@ -22,7 +22,7 @@
"id": "background",
"type": "background",
"layout": {"visibility": "visible"},
"paint": {"background-color": "rgba(146, 146, 142, 1)"}
"paint": {"background-color": "rgba(28, 28, 35, 1)"}
},
{
"id": "natural_earth",

View File

@@ -1,139 +1,99 @@
package com.kouros.navigation.model
import android.location.Location
import android.location.LocationManager
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
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.ManeuverType
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.location
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
open class RouteModel() {
lateinit var centerLocation: Location
data class RouteState(
val route: Route? = null,
val isNavigating: Boolean = false,
var destination: Place = Place(),
val arrived: Boolean = false,
var maneuverType: Int = 0,
var currentShapeIndex: Int = 0,
var distanceToStepEnd: Float = 0F,
var beginIndex: Int = 0,
var endIndex: Int = 0
)
lateinit var destination: Place
var routeState = RouteState()
var navigating = false
var arrived = false
var maneuverType = 0
/*
current shapeIndex
*/
var currentShapeIndex = 0
var distanceToStepEnd = 0F
var beginIndex = 0
var endIndex = 0
lateinit var route: Route
fun startNavigation(valhallaRoute: String) {
route = Route.Builder()
.route(valhallaRoute)
var route: Route
get() = routeState.route!!
set(value) {
routeState = routeState.copy(route = value)
}
fun startNavigation(routeString: String) {
val newRoute = Route.Builder()
.route(routeString)
.build()
centerLocation = createCenterLocation()
navigating = true
this.routeState = routeState.copy(
route = newRoute,
isNavigating = true
)
}
fun stopNavigation() {
route.clear()
navigating = false
currentShapeIndex = 0
distanceToStepEnd = 0F
beginIndex = 0
endIndex = 0
this.routeState = routeState.copy(
route = null,
isNavigating = false,
// destination = Place(),
arrived = false,
maneuverType = 0,
currentShapeIndex = 0,
distanceToStepEnd = 0F,
beginIndex = 0,
endIndex = 0
)
}
/**
* Calculates the geographic center of the route's GeoJSON data.
*
* @return A [Location] object representing the center point.
* @throws IllegalStateException if the calculated center does not have valid Point geometry.
*/
private fun createCenterLocation(): Location {
// 1. Create a FeatureCollection from the raw GeoJSON string.
val featureCollection = FeatureCollection.fromJson(route.routeGeoJson)
// 2. Calculate the center feature of the collection.
val centerFeature = TurfMeasurement.center(featureCollection)
// 3. Safely access and cast the geometry, throwing an informative error if it fails.
val centerPoint = centerFeature.geometry() as? Point
?: throw IllegalStateException("Center of GeoJSON is not a valid Point.")
// 4. Create and return the Location object.
return location(centerPoint.longitude(), centerPoint.latitude())
}
/**
* The remaining distance to the step, rounded to the nearest 10 units.
*/
val currentDistance: Double
get() {
// This is a more direct way to round to the nearest multiple of 10.
return (leftStepDistance() / 10.0).roundToInt() * 10.0
}
fun updateLocation(location: Location) {
var nearestDistance = 100000.0f
for (i in route.currentManeuverIndex..<route.maneuvers.size) {
val maneuver = route.maneuvers[i]
val beginShapeIndex = maneuver.beginShapeIndex
val endShapeIndex = maneuver.endShapeIndex
val distance = calculateDistance(beginShapeIndex, endShapeIndex, location)
var newShapeIndex = -1
// find nearest waypoint and current shape index
// start search at last shape index
for (i in routeState.currentShapeIndex..<route.waypoints.size) {
val waypoint = route.waypoints[i]
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
if (distance < nearestDistance) {
nearestDistance = distance
route.currentManeuverIndex = i
calculateCurrentShapeIndex(beginShapeIndex, endShapeIndex, location)
newShapeIndex = i
}
}
// find maneuver
// calculate distance to step end
findManeuver(newShapeIndex)
}
/** Calculates the index in a maneuver. */
private fun calculateCurrentShapeIndex(
beginShapeIndex: Int,
endShapeIndex: Int,
location: Location
) {
var nearestLocation = 100000.0f
for (i in currentShapeIndex..endShapeIndex) {
val waypoint = Location(LocationManager.GPS_PROVIDER)
waypoint.longitude = route.waypoints[i][0]
waypoint.latitude = route.waypoints[i][1]
val distance: Float = location.distanceTo(waypoint)
if (distance < nearestLocation) {
nearestLocation = distance
currentShapeIndex = i
beginIndex = beginShapeIndex
endIndex = endShapeIndex
distanceToStepEnd = 0F
val loc1 = Location(LocationManager.GPS_PROVIDER)
val loc2 = Location(LocationManager.GPS_PROVIDER)
if (i + 1 < route.waypoints.size) {
for (j in i + 1..endShapeIndex) {
loc1.longitude = route.waypoints[j - 1][0]
loc1.latitude = route.waypoints[j - 1][1]
loc2.longitude = route.waypoints[j][0]
loc2.latitude = route.waypoints[j][1]
private fun findManeuver(newShapeIndex: Int) {
for (i in route.currentManeuverIndex..<route.maneuvers.size) {
val maneuver = route.maneuvers[i]
if (maneuver.beginShapeIndex <= newShapeIndex && maneuver.endShapeIndex >= newShapeIndex) {
route = route.copy(currentManeuverIndex = i)
routeState.apply {
currentShapeIndex = newShapeIndex
beginIndex = maneuver.beginShapeIndex
endIndex = maneuver.endShapeIndex
distanceToStepEnd = 0F
// calculate shape distance to step end
for (j in newShapeIndex + 1..maneuver.endShapeIndex) {
val loc1 = location(route!!.waypoints[j - 1][0], route.waypoints[j - 1][1])
val loc2 = location(route.waypoints[j][0], route.waypoints[j][1])
distanceToStepEnd += loc1.distanceTo(loc2)
}
break
}
}
}
@@ -175,42 +135,6 @@ open class RouteModel() {
)
}
fun currentStepx(): StepData {
val maneuver = route.currentManeuver()
var text = ""
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
}
val distanceStepLeft = leftStepDistance()
when (distanceStepLeft) {
in 0.0..Constants.NEXT_STEP_THRESHOLD -> {
if (route.currentManeuverIndex < route.maneuvers.size) {
val maneuver = route.nextManeuver()
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
}
}
}
}
val type = if (hasArrived(maneuverType)) {
maneuver.type
} else {
ManeuverType.None.value
}
var routing: (Pair<Int, Int>) = maneuverIcon(type)
when (distanceStepLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
if (route.currentManeuverIndex < route.maneuvers.size) {
val maneuver = route.nextManeuver()
val maneuverType = maneuver.type
routing = maneuverIcon(maneuverType)
}
}
}
return StepData(text, distanceStepLeft, routing.first, routing.second, arrivalTime(), travelLeftDistance())
}
fun nextStep(): StepData {
val maneuver = route.nextManeuver()
val maneuverType = maneuver.type
@@ -220,30 +144,24 @@ open class RouteModel() {
when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
}
else -> {
if (maneuver.streetNames != null && maneuver.streetNames!!.isNotEmpty()) {
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
}
}
}
val routing: (Pair<Int, Int>) = maneuverIcon(maneuverType)
return StepData(text, distanceLeft, routing.first, routing.second, arrivalTime(), travelLeftDistance())
}
private fun calculateDistance(
beginShapeIndex: Int,
endShapeIndex: Int,
location: Location
): Float {
var nearestLocation = 100000.0f
for (i in beginShapeIndex..endShapeIndex) {
val polylineLocation = location(route.waypoints[i][0], route.waypoints[i][1])
val distance: Float = location.distanceTo(polylineLocation)
if (distance < nearestLocation) {
nearestLocation = distance
}
}
return nearestLocation
val routing: (Pair<Int, Int>) = maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
text,
distanceLeft,
routing.first,
routing.second,
arrivalTime(),
travelLeftDistance()
)
}
fun travelLeftTime(): Double {
@@ -252,10 +170,11 @@ open class RouteModel() {
val maneuver = route.maneuvers[i]
timeLeft += maneuver.time
}
if (endIndex > 0) {
if (routeState.endIndex > 0) {
val maneuver = route.currentManeuver()
val curTime = maneuver.time
val percent = 100 * (endIndex - currentShapeIndex) / (endIndex - beginIndex)
val percent =
100 * (routeState.endIndex - routeState.currentShapeIndex) / (routeState.endIndex - routeState.beginIndex)
val time = curTime * percent / 100
timeLeft += time
}
@@ -275,10 +194,12 @@ open class RouteModel() {
fun leftStepDistance(): Double {
val maneuver = route.currentManeuver()
var leftDistance = maneuver.length
if (endIndex > 0) {
leftDistance = (distanceToStepEnd / 1000).toDouble()
if (routeState.endIndex > 0) {
leftDistance = (routeState.distanceToStepEnd / 1000).toDouble()
}
return leftDistance * 1000
// The remaining distance to the step, rounded to the nearest 10 units.
return (leftDistance * 1000 / 10.0).roundToInt() * 10.0
}
/** Returns the left distance in km. */
@@ -288,10 +209,11 @@ open class RouteModel() {
val maneuver = route.maneuvers[i]
leftDistance += maneuver.length
}
if (endIndex > 0) {
if (routeState.endIndex > 0) {
val maneuver = route.currentManeuver()
val curDistance = maneuver.length
val percent = 100 * (endIndex - currentShapeIndex) / (endIndex - beginIndex)
val percent =
100 * (routeState.endIndex - routeState.currentShapeIndex) / (routeState.endIndex - routeState.beginIndex)
val time = curDistance * percent / 100
leftDistance += time
}
@@ -360,21 +282,21 @@ open class RouteModel() {
currentTurnIcon = R.drawable.ic_roundabout_ccw
}
}
maneuverType = type
routeState.maneuverType = type
return Pair(type, currentTurnIcon)
}
fun isNavigating(): Boolean {
return navigating
return routeState.isNavigating
}
fun isArrived(): Boolean {
return arrived
return routeState.arrived
}
fun hasArrived(type: Int): Boolean {
return type == ManeuverType.DestinationRight.value
|| maneuverType == ManeuverType.Destination.value
|| maneuverType == ManeuverType.DestinationLeft.value
|| routeState.maneuverType == ManeuverType.Destination.value
|| routeState.maneuverType == ManeuverType.DestinationLeft.value
}
}

View File

@@ -241,7 +241,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
savePlace(place)
}
fun savePlace(place: Place) {
private fun savePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
@@ -258,6 +258,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val current = LocalDateTime.now(ZoneOffset.UTC)
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
placeBox.put(place)
println("Save Recent $place")
} catch (e: Exception) {
e.printStackTrace()
}
@@ -333,4 +334,21 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
return results.toMutableStateList()
}
fun loadRecentPlace(): SnapshotStateList<Place?> {
val results = listOf<Place>()
try {
val placeBox = boxStore.boxFor(Place::class)
val query = placeBox
.query(Place_.name.notEqual("").and(Place_.category.equal(Constants.RECENT)))
.orderDesc(Place_.lastDate)
.build()
val results = query.find()
query.close()
return results.toMutableStateList()
} catch (e: Exception) {
e.printStackTrace()
}
return results.toMutableStateList()
}
}

View File

@@ -9,7 +9,9 @@ import com.kouros.navigation.data.GeoJsonFeature
import com.kouros.navigation.data.GeoJsonFeatureCollection
import com.kouros.navigation.data.GeoJsonLineString
import kotlinx.serialization.json.Json
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc
import java.lang.Math.toDegrees
import java.lang.Math.toRadians
@@ -19,13 +21,15 @@ import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import kotlin.math.absoluteValue
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
object NavigationUtils {
@@ -52,6 +56,29 @@ object NavigationUtils {
apply()
}
}
fun getIntKeyValue(context: Context, key: String) : Int {
return context
.getSharedPreferences(
SHARED_PREF_KEY,
Context.MODE_PRIVATE
)
.getInt(key, 0)
}
fun setIntKeyValue(context: Context, `val`: Int, key: String) {
context
.getSharedPreferences(
SHARED_PREF_KEY,
Context.MODE_PRIVATE
)
.edit {
putInt(
key, `val`
)
apply()
}
}
fun snapLocation(location: Location, stepCoordinates: List<Point>) : Location {
val newLocation = Location(location)
val oldPoint = Point.fromLngLat(location.longitude, location.latitude)
@@ -101,6 +128,26 @@ object NavigationUtils {
return coordinates
}
/**
* Calculates the geographic center of the route's GeoJSON data.
*
* @return A [Location] object representing the center point.
* @throws IllegalStateException if the calculated center does not have valid Point geometry.
*/
fun createCenterLocation(routeGeoJson: String): Location {
// 1. Create a FeatureCollection from the raw GeoJSON string.
val featureCollection = FeatureCollection.fromJson(routeGeoJson)
// 2. Calculate the center feature of the collection.
val centerFeature = TurfMeasurement.center(featureCollection)
// 3. Safely access and cast the geometry, throwing an informative error if it fails.
val centerPoint = centerFeature.geometry() as? Point
?: throw IllegalStateException("Center of GeoJSON is not a valid Point.")
// 4. Create and return the Location object.
return location(centerPoint.longitude(), centerPoint.latitude())
}
fun createGeoJson(lineCoordinates: List<List<Double>>): String {
val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates)
@@ -210,4 +257,16 @@ fun formatDateTime(time: Long): String {
fun Double.round(numFractionDigits: Int): Double {
val factor = 10.0.pow(numFractionDigits.toDouble())
return (this * factor).roundToInt() / factor
}
fun duration(preview: Boolean, bearing: Double, lastBearing: Double): Duration {
if (preview) {
return 3.seconds
}
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
2.seconds
} else {
1.seconds
}
return cameraDuration
}