This commit is contained in:
Dimitris
2026-03-12 15:34:34 +01:00
parent 61ce09f393
commit 619ceb9f83
28 changed files with 1815 additions and 634 deletions

View File

@@ -33,7 +33,6 @@ import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom
@@ -46,6 +45,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
@@ -261,6 +261,7 @@ class SurfaceRenderer(
val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
val dark = darkMode == 1 || darkMode == 2 && carContext.isDarkMode
MapLibre(
cameraState,
@@ -271,7 +272,7 @@ class SurfaceRenderer(
speedCameras,
showBuildings
)
ShowPosition(cameraState, position, paddingValues, darkMode)
ShowPosition(cameraState, position, paddingValues, dark)
}
/**
@@ -283,18 +284,18 @@ class SurfaceRenderer(
cameraState: CameraState,
position: CameraPosition?,
paddingValues: PaddingValues,
darkMode: Int
darkMode: Boolean
) {
val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
val currentSpeed: Float? by speed.observeAsState()
val speed: Int? by maxSpeed.observeAsState()
val maximumSpeed: Int? by maxSpeed.observeAsState()
val streetName: String? by street.observeAsState()
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
DrawNavigationImages(
paddingValues,
currentSpeed,
speed!!,
maximumSpeed!!,
width,
height,
streetName,
@@ -340,7 +341,7 @@ class SurfaceRenderer(
updateCameraPosition(
cameraPosition.value!!.bearing,
newZoom,
cameraPosition.value!!.target,
cameraPosition.value!!.target, tilt
)
}
}
@@ -379,7 +380,7 @@ class SurfaceRenderer(
updateCameraPosition(
bearing,
zoom,
Position(location.longitude, location.latitude)
Position(location.longitude, location.latitude), tilt
)
lastBearing = cameraPosition.value!!.bearing
lastLocation = location
@@ -387,11 +388,19 @@ class SurfaceRenderer(
}
}
/**
* Sets route data for active navigation and switches to VIEW mode.
*/
fun setRouteData() {
routeData.value = routeModel.curRoute.routeGeoJson
viewStyle = ViewStyle.VIEW
}
/**
* Updates camera position with new bearing, zoom, and target.
* Posts update to LiveData for UI observation.
*/
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position, tilt: Double) {
synchronized(this) {
cameraPosition.postValue(
cameraPosition.value!!.copy(
@@ -408,9 +417,15 @@ class SurfaceRenderer(
/**
* Sets route data for active navigation and switches to VIEW mode.
*/
fun setRouteData() {
routeData.value = routeModel.curRoute.routeGeoJson
fun clearRouteData() {
routeData.value = ""
viewStyle = ViewStyle.VIEW
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = 16.0
)
)
tilt = TILT
}
/**
@@ -424,18 +439,21 @@ class SurfaceRenderer(
* Sets up route preview mode with overview camera position.
* Calculates appropriate zoom based on route distance.
*/
fun setPreviewRouteData(routeModel: RouteModel) {
fun setPreviewRouteData(routeModel: RouteCarModel) {
viewStyle = ViewStyle.PREVIEW
with(routeModel) {
routeData.value = curRoute.routeGeoJson
centerLocation = curRoute.centerLocation
previewDistance = curRoute.summary.distance
}
tilt = 0.0
updateCameraPosition(
0.0,
previewZoom(previewDistance),
Position(centerLocation.longitude, centerLocation.latitude)
previewZoom(centerLocation, previewDistance),
Position(centerLocation.longitude, centerLocation.latitude), 0.0
)
}
/**
@@ -448,7 +466,7 @@ class SurfaceRenderer(
updateCameraPosition(
0.0,
14.0,
target = Position(location.longitude, location.latitude)
target = Position(location.longitude, location.latitude), tilt
)
}
}

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -19,6 +20,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
@@ -33,6 +35,8 @@ import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.utils.isMetricSystem
import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
@@ -59,6 +63,7 @@ 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.BoundingBox
import org.maplibre.spatialk.geojson.Position
@@ -296,7 +301,7 @@ fun DrawNavigationImages(
width: Int,
height: Int,
streetName: String?,
darkMode: Int,
darkMode: Boolean,
) {
NavigationImage(padding, width, height, streetName, darkMode)
if (speed != null) {
@@ -314,7 +319,7 @@ fun NavigationImage(
width: Int,
height: Int,
streetName: String?,
darkMode: Int
darkMode: Boolean
) {
val imageSize = (height / 8)
@@ -323,9 +328,9 @@ fun NavigationImage(
val textMeasurerStreet = rememberTextMeasurer()
val street = streetName.toString()
val styleStreet = TextStyle(
fontSize = 14.sp,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = if (darkMode == 1) Color.White else navigationColor,
color = if (darkMode) Color.White else navigationColor,
)
val textLayoutStreet = remember(street) {
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
@@ -348,28 +353,31 @@ fun NavigationImage(
.size(imageSize.dp, imageSize.dp)
.scale(scaleX = 1f, scaleY = 0.7f),
)
Canvas(
modifier = Modifier
.size((width / 5).dp, (height/4).dp)
.size(textLayoutStreet.size.width.dp, textLayoutStreet.size.height.dp * 6 )
) {
if (!streetName.isNullOrEmpty()) {
if (street.isNotEmpty()) {
val topLeftX = center.x - textLayoutStreet.size.width / 2
val topLeftY = center.y + textLayoutStreet.size.height
drawRoundRect(
topLeft = Offset(
x = center.x - textLayoutStreet.size.width / 2 ,
y = center.y + textLayoutStreet.size.height,
x = topLeftX ,
y = topLeftY,
),
color = if (darkMode == 1) navigationColor else Color.White,
color = if (darkMode) navigationColor else Color.White,
cornerRadius = CornerRadius(x = 10f, y = 10f),
)
drawText(
textMeasurer = textMeasurerStreet,
text = streetName,
text = street,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = styleStreet,
topLeft = Offset(
x = center.x - textLayoutStreet.size.width / 2,
y = center.y + textLayoutStreet.size.height + 10,
x = topLeftX,
y = topLeftY + 10,
)
)
}
@@ -384,6 +392,7 @@ private fun CurrentSpeed(
curSpeed: Float,
maxSpeed: Int
) {
val radius = 34
Box(
modifier = Modifier
@@ -395,9 +404,11 @@ private fun CurrentSpeed(
) {
val textMeasurerSpeed = rememberTextMeasurer()
val textMeasurerKm = rememberTextMeasurer()
val speed = (curSpeed * 3.6).toInt().toString()
val kmh = "km/h"
val speed = if (isMetricSystem()) (curSpeed * 3.6).toInt().toString() else (curSpeed * 3.6 * 0.6214).toInt().toString()
val kmh = if (isMetricSystem()) "km/h" else "mph"
val styleSpeed = TextStyle(
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
@@ -433,7 +444,7 @@ private fun CurrentSpeed(
)
drawText(
textMeasurer = textMeasurerKm,
text = "km/h",
text = kmh,
style = styleKm,
topLeft = Offset(
x = center.x - textLayoutKm.size.width / 2,

View File

@@ -17,30 +17,32 @@ class Simulation {
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return
simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
simulationJob = lifecycleScope.launch {
for (point in points) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1]
longitude = point[0]
bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
if (routeModel.navState.route.isRouteValid()) {
val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return
simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
simulationJob = lifecycleScope.launch {
for (point in points) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1]
longitude = point[0]
bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
delay(1000)
lastLocation = fakeLocation
}
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 2 seconds)
delay(1000)
lastLocation = fakeLocation
routeModel.stopNavigation()
}
routeModel.stopNavigation()
}
}

View File

@@ -116,8 +116,9 @@ class CategoryScreen(
} else {
row.addText("${(it.distance / 1000).round(1)} km")
}
if (category == Constants.CHARGING_STATION) {
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
if (category == CHARGING_STATION) {
if (it.tags.socketType2 != null)
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
} else {
row.addText(carText("${it.tags.openingHours}"))
}
@@ -134,7 +135,7 @@ class CategoryScreen(
setResult(
Place(
name = name,
category = Constants.CHARGING_STATION,
category = CHARGING_STATION,
latitude = it.lat,
longitude = it.lon
)

View File

@@ -377,7 +377,6 @@ class NavigationScreen(
*/
private fun stopAction(): Action {
return Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
@@ -531,6 +530,8 @@ class NavigationScreen(
* Pushes the search screen and handles the search result.
*/
private fun startSearchScreen() {
navigationViewModel.recentPlaces.value = emptyList()
navigationViewModel.previewRoute.value = ""
screenManager
.pushForResult(
SearchScreen(
@@ -558,16 +559,22 @@ class NavigationScreen(
* Loads a route to the specified place and sets it as the destination.
*/
fun navigateToPlace(place: Place) {
val preview = navigationViewModel.previewRoute.value
navigationType = NavigationType.VIEW
val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place)
currentNavigationLocation = location
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
if (preview.isNullOrEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
navigationViewModel.route.value = preview
}
routeModel.navState = routeModel.navState.copy(destination = place)
invalidate()
}

View File

@@ -17,7 +17,6 @@ import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel
@@ -27,58 +26,52 @@ import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
class PlaceListScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val category: String,
private val navigationViewModel: NavigationViewModel
private val navigationViewModel: NavigationViewModel,
private val places: List<Place>
) : Screen(carContext) {
var places = listOf<Place>()
val observer = Observer<List<Place>> { newPlaces ->
places = newPlaces
invalidate()
}
val routeModel = RouteCarModel()
val observerAddress = Observer<List<Place>> { newContacts ->
places = newContacts
invalidate()
var place = Place()
val previewObserver = Observer<String> { route ->
if (route.isNotEmpty()) {
val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
surfaceRenderer,
place,
navigationViewModel,
routeModel = routeModel
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
}
init {
if (category == RECENT) {
navigationViewModel.places.observe(this, observer)
}
if (category == CONTACTS) {
navigationViewModel.contactAddress.observe(this, observerAddress)
}
if (category == FAVORITES) {
navigationViewModel.favorites.observe(this, observer)
}
loadPlaces()
}
fun loadPlaces() {
if (category == RECENT) {
navigationViewModel.loadRecentPlaces(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
if (category == CONTACTS) {
navigationViewModel.loadContacts(carContext)
}
if (category == FAVORITES) {
navigationViewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
navigationViewModel.previewRoute.observe(this, previewObserver)
}
override fun onGetTemplate(): Template {
@@ -91,10 +84,10 @@ class PlaceListScreen(
""
}
val row = Row.Builder()
// .setImage(contactIcon(it.avatar, it.category))
// .setImage(contactIcon(it.avatar, it.category))
.setTitle("$street ${it.city}")
.setOnClickListener {
val place = Place(
place = Place(
0,
it.name,
it.category,
@@ -103,29 +96,22 @@ class PlaceListScreen(
it.postalCode,
it.city,
it.street,
// avatar = null
// avatar = null
)
val location = location(place.longitude, place.latitude)
navigationViewModel.loadPreviewRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
surfaceRenderer,
place,
navigationViewModel
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
if (category != CONTACTS) {
row.addText(SpannableString(" ").apply {
setSpan(
DistanceSpan.create(
Distance.create(
(it.distance/1000).toDouble(),
(it.distance / 1000).toDouble(),
Distance.UNIT_KILOMETERS
)
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
@@ -184,4 +170,10 @@ class PlaceListScreen(
}
return CarIcon.Builder(IconCompat.createWithContentUri(avatar)).build()
}
fun loadPlaces() {
if (category == CONTACTS) {
navigationViewModel.loadContacts(carContext)
}
}
}

View File

@@ -2,10 +2,12 @@ package com.kouros.navigation.car.screen
import android.os.CountDownTimer
import android.text.SpannableString
import androidx.activity.OnBackPressedCallback
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip
@@ -20,88 +22,79 @@ import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.Lifecycle
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import com.kouros.navigation.model.RouteModel
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.math.min
/** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */
class RoutePreviewScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private var destination: Place,
private val navigationViewModel: NavigationViewModel
private val navigationViewModel: NavigationViewModel,
private val routeModel: RouteCarModel
) :
Screen(carContext) {
private var isFavorite = false
val routeModel = RouteCarModel()
val maxListItems: Int = 3
val navigationUtils = NavigationUtils(carContext)
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
invalidate()
}
}
init {
navigationViewModel.previewRoute.observe(this, observer)
val location = location(destination.longitude, destination.latitude)
navigationViewModel.loadPreviewRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback)
}
override fun onGetTemplate(): Template {
val navigateActionIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.navigation_48px
)
).build()
val navigateAction = Action.Builder()
.setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate() }
.build()
val itemListBuilder = ItemList.Builder()
var i = 0
routeModel.route.routes.forEach { _ ->
itemListBuilder.addItem(createRow(i++, navigateAction))
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
val listLimit = min(
maxListItems,
carContext.getCarService(ConstraintManager::class.java)
.getContentLimit(
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
)
)
var index = 0
routeModel.route.routes.forEach { route ->
itemListBuilder.addItem(createRow(route, index++))
}
}
val header = Header.Builder()
.setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.route_preview))
.addEndHeaderAction(
if (routeModel.route.routes.size == 1) {
header.addEndHeaderAction(
favoriteAction()
)
.addEndHeaderAction(
header.addEndHeaderAction(
deleteFavoriteAction()
)
.build()
}
val message =
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
createRouteText(0)
createRouteText(routeModel.route.routes.first())
} else {
CarText.Builder("Wait")
.build()
@@ -110,7 +103,7 @@ class RoutePreviewScreen(
val timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
onNavigate()
onNavigate(0)
}
}
timer.start()
@@ -118,18 +111,27 @@ class RoutePreviewScreen(
val content = if (routeModel.route.routes.size > 1) {
ListTemplate.Builder()
.setHeader(header)
.setHeader(header.build())
.setSingleList(itemListBuilder.build())
.build()
} else {
val navigateActionIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.navigation_48px
)
).build()
val navigateAction = Action.Builder()
.setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate(0) }
.build()
MessageTemplate.Builder(
message
)
.setHeader(header)
.setHeader(header.build())
.addAction(navigateAction)
.setLoading(message.toString() == "Wait")
.build()
}
return MapWithContentTemplate.Builder()
.setContentTemplate(content)
@@ -176,7 +178,7 @@ class RoutePreviewScreen(
private fun deleteFavoriteAction(): Action = Action.Builder()
.setOnClickListener {
if (isFavorite) {
navigationViewModel.deleteFavorite(carContext,destination)
navigationViewModel.deleteFavorite(carContext, destination)
}
isFavorite = !isFavorite
finish()
@@ -192,10 +194,10 @@ class RoutePreviewScreen(
)
.build()
private fun createRouteText(index: Int): CarText {
val time = routeModel.route.routes[index].summary.duration
private fun createRouteText(route: Routes): CarText {
val time = route.summary.duration
val length =
BigDecimal((routeModel.route.routes[index].summary.distance) / 1000).setScale(
BigDecimal((route.summary.distance) / 1000).setScale(
1,
RoundingMode.HALF_EVEN
)
@@ -207,18 +209,43 @@ class RoutePreviewScreen(
.build()
}
private fun createRow(index: Int, action: Action): Row {
val route = createRouteText(index)
val titleText = "$index"
return Row.Builder()
.setTitle(route)
.setOnClickListener { onRouteSelected(index) }
.addText(titleText)
.addAction(action)
private fun createRow(route: Routes, index: Int): Row {
val navigateActionIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.navigation_48px
)
).build()
val navigateAction = Action.Builder()
.setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate(index) }
.build()
val routeText = createRouteText(route)
var street = ""
var maxDistance = 0.0
routeModel.route.routes[index].legs.first().steps.forEach {
if (it.distance > maxDistance) {
maxDistance = it.distance
street = it.street
}
}
val delay = (route.summary.trafficDelay / 60).toInt().toString()
val row = Row.Builder()
.setTitle(routeText)
.setOnClickListener { onRouteSelected(index) }
.addText(street)
.addAction(navigateAction)
if (route.summary.trafficDelay > 60) {
row.addText("$delay min")
}
return row.build()
}
private fun onNavigate() {
private fun onNavigate(index: Int) {
destination.routeIndex = index
setResult(destination)
finish()
}

View File

@@ -16,7 +16,9 @@ import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.CATEGORIES
import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel
@@ -30,11 +32,13 @@ class SearchScreen(
var isSearchComplete: Boolean = false
var category = ""
var categories: List<Category> = listOf(
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
// Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)),
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites))
Category(id = RECENT, name = carContext.getString(R.string.recent_destinations)),
// Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
Category(id = CATEGORIES, name = carContext.getString(R.string.category_title)),
Category(id = FAVORITES, name = carContext.getString(R.string.favorites))
)
lateinit var searchResult: List<SearchResult>
@@ -44,8 +48,50 @@ class SearchScreen(
invalidate()
}
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
if (newPlaces.isNotEmpty()) {
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
RECENT,
navigationViewModel,
newPlaces
)
) { obj: Any? ->
surfaceRenderer.clearRouteData()
if (obj != null) {
setResult(obj)
finish()
}
}
}
}
val observerFavorites = Observer<List<Place>> { newPlaces ->
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
FAVORITES,
navigationViewModel,
newPlaces
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
init {
navigationViewModel.searchPlaces.observe(this, observer)
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
navigationViewModel.favorites.observe(this, observerFavorites)
}
override fun onGetTemplate(): Template {
@@ -62,7 +108,7 @@ class SearchScreen(
.setTitle(it.name)
.setImage(categoryIcon(it.id))
.setOnClickListener {
if (it.id == Constants.CATEGORIES) {
if (it.id == CATEGORIES) {
screenManager
.pushForResult(
CategoriesScreen(
@@ -78,20 +124,20 @@ class SearchScreen(
}
}
} else {
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
it.id,
navigationViewModel
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
if (it.id == RECENT) {
navigationViewModel.loadRecentPlaces(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
if (it.id == FAVORITES) {
navigationViewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
}
}
.setBrowsable(true)
@@ -122,13 +168,15 @@ class SearchScreen(
}
fun categoryIcon(category: String?): CarIcon {
val resId : Int = when (category) {
Constants.RECENT -> {
val resId: Int = when (category) {
RECENT -> {
R.drawable.ic_place_white_24dp
}
Constants.FAVORITES -> {
FAVORITES -> {
R.drawable.ic_favorite_white_24dp
}
else -> {
R.drawable.navigation_48px
}
@@ -148,7 +196,7 @@ class SearchScreen(
searchResult.forEach {
searchItemListBuilder.addItem(
Row.Builder()
.setTitle("${(it.distance/1000).toInt()} km ${it.displayName} ")
.setTitle("${(it.distance / 1000).toInt()} km ${it.displayName} ")
.setOnClickListener {
navigateToPlace(it)
}

View File

@@ -43,7 +43,8 @@ data class Place(
var street: String? = null,
var distance: Float = 0F,
//var avatar: Uri? = null,
var lastDate: Long = 0
var lastDate: Long = 0,
var routeIndex: Int = 0
)
data class ContactData(

View File

@@ -23,12 +23,14 @@ data class Route(
val routeEngine: Int,
val routes: List<com.kouros.navigation.data.route.Routes>,
var currentStepIndex: Int = 0,
var routeIndex : Int = 0,
) {
data class Builder(
var routeEngine: Int = 0,
var summary: Summary = Summary(),
var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(),
var routeIndex : Int = 0
) {
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
@@ -38,6 +40,8 @@ data class Route(
}
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun routeIndex(routeIndex: Int) = apply { this.routeIndex = routeIndex}
fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create()
@@ -68,6 +72,7 @@ data class Route(
return Route(
routeEngine = this.routeEngine,
routes = this.routes,
routeIndex = this.routeIndex
)
}
@@ -75,6 +80,7 @@ data class Route(
return Route(
routeEngine = 0,
routes = emptyList(),
routeIndex = 0
)
}
}
@@ -82,7 +88,7 @@ data class Route(
fun legs(): List<Leg> {
return if (routes.isNotEmpty()) {
routes.first().legs
routes[routeIndex].legs
} else {
emptyList()
}

View File

@@ -58,6 +58,7 @@ class TomTomRepository : NavigationRepository() {
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
"&instructionsType=text&language=$language&sectionType=lanes" +
"&routeRepresentation=encodedPolyline" +
"&maxAlternatives=2" +
"&vehicleEngineType=combustion$filter&key=$tomtomApiKey"
return fetchUrl(
url,

View File

@@ -3,10 +3,8 @@ package com.kouros.navigation.model
//import com.kouros.navigation.data.Preferences.boxStore
import android.content.Context
import android.location.Location
import android.util.Log
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -58,7 +56,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
/** LiveData containing list of recent navigation destinations */
val places: MutableLiveData<List<Place>> by lazy {
val recentPlaces: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
@@ -144,11 +142,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val settingsRepository = getSettingsRepository(context)
val rp = settingsRepository.recentPlacesFlow.first()
val gson = GsonBuilder().serializeNulls().create()
val recentPlaces = gson.fromJson(rp, Places::class.java)
val places = gson.fromJson(rp, Places::class.java)
val pl = mutableListOf<Place>()
var id: Long = 0
if (rp.isNotEmpty()) {
for (place in recentPlaces.places) {
for (place in places.places) {
if (place.category.equals(Constants.RECENT)) {
val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) {
@@ -167,7 +165,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
}
places.postValue(pl)
recentPlaces.postValue(pl)
} catch (e: Exception) {
e.printStackTrace()
}

View File

@@ -34,6 +34,7 @@ open class RouteModel {
route = Route.Builder()
.routeEngine(navState.routingEngine)
.route(routeString)
.routeIndex(navState.currentRouteIndex)
.build()
)
if (hasLegs()) {
@@ -42,7 +43,7 @@ open class RouteModel {
}
fun hasLegs(): Boolean {
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty()
return navState.route.routes.isNotEmpty() && navState.route.routes[navState.currentRouteIndex].legs.isNotEmpty()
}
fun stopNavigation() {

View File

@@ -9,8 +9,11 @@ import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.lang.Math.toDegrees
import java.lang.Math.toRadians
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
@@ -19,6 +22,8 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.ln
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.time.Duration
@@ -29,7 +34,7 @@ object NavigationUtils {
fun getViewModel(context: Context): NavigationViewModel {
val repository = getSettingsRepository(context)
val routeEngine = runBlocking { repository.routingEngineFlow.first() }
val routeEngine = runBlocking { repository.routingEngineFlow.first() }
return when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
@@ -53,15 +58,36 @@ fun calculateZoom(speed: Double?): Double {
return zoom
}
fun previewZoom(previewDistance: Double): Double {
when (previewDistance / 1000) {
in 0.0..10.0 -> return 13.5
in 10.0..20.0 -> return 11.5
in 20.0..30.0 -> return 10.5
}
return 9.5
fun previewZoom(centerLocation: Location, previewDistance: Double): Double {
return calculateZoomFromBoundingBox(centerLocation, previewDistance / 1000)
}
fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Double): Double {
val earthRadius = 6371.0
val maxLat = centerLocation.latitude + toDegrees(previewDistance / earthRadius)
val minLat = centerLocation.latitude - toDegrees(previewDistance / earthRadius)
val maxLon = centerLocation.longitude + toDegrees(previewDistance / earthRadius / cos(toRadians(centerLocation.latitude)))
val minLon = centerLocation.longitude - toDegrees(previewDistance / earthRadius / cos(toRadians(centerLocation.latitude)))
var zoomLevel: Double
val latDiff = maxLat - minLat
val lngDiff = maxLon - minLon
val maxDiff = if(lngDiff > latDiff) lngDiff else latDiff
if (maxDiff < 360 / 2.0.pow(20.0)) {
zoomLevel = 21.0
} else {
zoomLevel = (-1 * ((ln(maxDiff) / ln(2.0)) - (ln(360.0) / ln(2.0))));
if (zoomLevel < 1)
zoomLevel = 1.0;
}
return zoomLevel + 1.2
}
fun calculateTilt(newZoom: Double, tilt: Double): Double =
if (newZoom < 13) {
0.0
@@ -119,10 +145,10 @@ fun isMetricSystem(): Boolean {
return !setOf("US", "UK", "LR", "MM").contains(country)
}
fun formattedDistance(distanceMode : Int, distance: Double): Pair<Double, Int> {
fun formattedDistance(distanceMode: Int, distance: Double): Pair<Double, Int> {
var currentDistance = distance
var displayUnit: Int
if (distanceMode == 1 || distanceMode == 0 && isMetricSystem()) {
if (distanceMode == 1 || distanceMode == 0 && isMetricSystem()) {
displayUnit = if (currentDistance > 1000.0) {
currentDistance /= 1000.0
Distance.UNIT_KILOMETERS

File diff suppressed because it is too large Load Diff