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

@@ -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
}