From 33f5ef4f3471445e76fc3ce1e101f662d3204f53 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Thu, 20 Nov 2025 10:27:33 +0100 Subject: [PATCH] Serialize Json --- .../com/kouros/navigation/MainActivity.kt | 216 ++++++++------- common/car/build.gradle.kts | 1 + .../com/kouros/navigation/car/LocationPuck.kt | 257 ++++++++++++++++++ .../navigation/car/NavigationSession.kt | 18 +- .../kouros/navigation/car/SurfaceRenderer.kt | 44 +-- .../car/navigation/RouteCarModel.kt | 52 ++-- .../navigation/car/screen/NavigationScreen.kt | 3 +- .../navigation/car/screen/PlaceListScreen.kt | 2 + .../car/screen/RoutePreviewScreen.kt | 8 +- .../navigation/car/screen/SearchScreen.kt | 7 +- common/data/build.gradle.kts | 16 +- .../java/com/kouros/navigation/data/Data.kt | 10 +- .../navigation/data/NavigationRepository.kt | 3 +- .../java/com/kouros/navigation/data/Route.kt | 115 ++++++++ .../kouros/navigation/data/valhalla/Legs.kt | 17 ++ .../navigation/data/valhalla/Locations.kt | 19 ++ .../navigation/data/valhalla/Maneuvers.kt | 29 ++ .../navigation/data/valhalla/Summary.kt | 25 ++ .../kouros/navigation/data/valhalla/Trip.kt | 21 ++ .../navigation/data/valhalla/ValhallaJson.kt | 16 ++ .../com/kouros/navigation/model/RouteModel.kt | 176 +++++------- .../com/kouros/navigation/model/ViewModel.kt | 5 +- .../navigation/utils/NavigationUtils.kt | 235 ++++++++-------- gradle/libs.versions.toml | 15 +- 24 files changed, 919 insertions(+), 391 deletions(-) create mode 100644 common/car/src/main/java/com/kouros/navigation/car/LocationPuck.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/Route.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/valhalla/Legs.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/valhalla/Locations.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/valhalla/Maneuvers.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/valhalla/Summary.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/valhalla/Trip.kt create mode 100644 common/data/src/main/java/com/kouros/navigation/data/valhalla/ValhallaJson.kt diff --git a/app/src/main/java/com/kouros/navigation/MainActivity.kt b/app/src/main/java/com/kouros/navigation/MainActivity.kt index 68c6073..d80ee0f 100644 --- a/app/src/main/java/com/kouros/navigation/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/MainActivity.kt @@ -1,19 +1,3 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.kouros.navigation import android.Manifest @@ -21,7 +5,6 @@ import android.annotation.SuppressLint import android.location.Location import android.location.LocationManager import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -47,7 +30,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -68,14 +51,16 @@ import androidx.lifecycle.Observer import com.example.places.ui.theme.PlacesTheme import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.kouros.android.cars.carappservice.R + import com.kouros.navigation.data.Category import com.kouros.navigation.data.Constants -import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.StepData import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.ViewModel -import com.kouros.navigation.utils.NavigationUtils +import com.kouros.navigation.utils.NavigationUtils.snapLocation +import com.kouros.navigation.utils.calculateZoom import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.maplibre.compose.camera.CameraPosition @@ -83,14 +68,14 @@ import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.LineLayer +import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuckColors +import org.maplibre.compose.location.LocationPuckSizes +import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberUserLocationState -import org.maplibre.compose.map.GestureOptions -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.getBaseSource import org.maplibre.compose.sources.rememberGeoJsonSource @@ -99,24 +84,24 @@ import org.maplibre.spatialk.geojson.Position import kotlin.time.Duration.Companion.seconds -val routeData = MutableLiveData("") - class MainActivity : ComponentActivity() { + val routeData = MutableLiveData("") + val vieModel = ViewModel(NavigationRepository()) val routeModel = RouteModel() - var tilt = 0.0 - - val curLocation = Location(LocationManager.GPS_PROVIDER) + var tilt = 50.0 val instruction: MutableLiveData by lazy { MutableLiveData() } + var lastLocation = Location(LocationManager.GPS_PROVIDER) + val observer = Observer { newRoute -> routeModel.startNavigation(newRoute) - routeData.value = routeModel.route + routeData.value = routeModel.route.routeGeoJson } val cameraPosition = MutableLiveData( @@ -126,40 +111,14 @@ class MainActivity : ComponentActivity() { ) ) + var locationIndex = 0 + + var test = false + init { vieModel.route.observe(this, observer) } - fun updateLocation(location: org.maplibre.compose.location.Location?) { - if (location != null) { - if (routeModel.isNavigating()) { - instruction.value = routeModel.currentStep() - } - val zoom = NavigationUtils().calculateZoom(location.speed) - cameraPosition.postValue( - cameraPosition.value!!.copy( - zoom = zoom, - target = location.position - ), - ) - } - } - - fun test() { - for (i in 0.. FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building") RouteLayer(route) } + LocationPuck( + idPrefix = "user-location1", + locationState = userLocationState, + cameraState = cameraState, + accuracyThreshold = 10f, + showBearing = false, + sizes = LocationPuckSizes(dotRadius = 10.dp), + colors = LocationPuckColors( + dotFillColorCurrentLocation = Color.Cyan, + accuracyStrokeColor = Color.Green + ) + ) } - LaunchedEffect(position) { + LocationTrackingEffect( + locationState = userLocationState, + ) { + //cameraState.updateFromLocation() cameraState.animateTo( finalPosition = CameraPosition( bearing = position!!.bearing, @@ -354,9 +316,22 @@ class MainActivity : ComponentActivity() { target = position!!.target, tilt = tilt ), - duration = 3.seconds + duration = 1.seconds ) } + +// LaunchedEffect(position) { +// println("CameraPosition ${position!!.target.latitude}") +// cameraState.animateTo( +// finalPosition = CameraPosition( +// bearing = position!!.bearing, +// zoom = position!!.zoom, +// target = position!!.target, +// tilt = tilt +// ), +// duration = 3.seconds +// ) +// } } @Composable @@ -368,17 +343,66 @@ class MainActivity : ComponentActivity() { id = "routes-casing", source = routes, color = const(Color.White), - width = const(6.dp), + width = const(10.dp), ) LineLayer( id = "routes", source = routes, color = const(Color.Blue), - width = const(4.dp), + width = const(8.dp), ) } } + fun updateLocation(location: org.maplibre.compose.location.Location?) { + if (location != null) { + if (routeModel.isNavigating()) { + routeModel.updateLocation(lastLocation) + instruction.value = routeModel.currentStep() + } + val zoom = calculateZoom(location.speed) + cameraPosition.postValue( + cameraPosition.value!!.copy( + zoom = zoom, + target = location.position + ), + ) + } + } + + + fun updateTestLocation(location: Location) { + var snapedLocation = location + var bearing: Double + if (routeModel.isNavigating()) { + snapedLocation = snapLocation(location, routeModel.maneuverLocations()) + bearing = routeModel.currentStep().bearing + routeModel.updateLocation(snapedLocation) + instruction.value = routeModel.currentStep() + } else { + bearing = cameraPosition.value!!.bearing + } + val zoom = calculateZoom(snapedLocation.speed.toDouble()) + cameraPosition.postValue( + cameraPosition.value!!.copy( + bearing = bearing, + zoom = zoom, + target = Position(snapedLocation.longitude, snapedLocation.latitude) + ), + ) + } + + fun test() { + if (routeModel.isNavigating() && locationIndex < routeModel.route.waypoints.size) { + val loc = routeModel.route.waypoints[locationIndex] + lastLocation.longitude = loc[0] + lastLocation.latitude = loc[1] + updateTestLocation(lastLocation) + Thread.sleep(1_000) + locationIndex++ + } + } + @Composable fun PlaceList(viewModel: ViewModel = koinViewModel()) { var categories: List diff --git a/common/car/build.gradle.kts b/common/car/build.gradle.kts index 4605d8c..87fb36b 100644 --- a/common/car/build.gradle.kts +++ b/common/car/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(libs.androidx.ui) implementation(libs.maplibre.compose) //implementation(libs.maplibre.composeMaterial3) + implementation(project(":common:data")) implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.compose.foundation) diff --git a/common/car/src/main/java/com/kouros/navigation/car/LocationPuck.kt b/common/car/src/main/java/com/kouros/navigation/car/LocationPuck.kt new file mode 100644 index 0000000..2d8939e --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/LocationPuck.kt @@ -0,0 +1,257 @@ +package com.kouros.navigation.car + +import android.location.Location +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.Path +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.VectorPainter +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.maplibre.compose.camera.CameraState +import org.maplibre.compose.expressions.dsl.asNumber +import org.maplibre.compose.expressions.dsl.condition +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.expressions.dsl.div +import org.maplibre.compose.expressions.dsl.dp +import org.maplibre.compose.expressions.dsl.feature +import org.maplibre.compose.expressions.dsl.gt +import org.maplibre.compose.expressions.dsl.image +import org.maplibre.compose.expressions.dsl.minus +import org.maplibre.compose.expressions.dsl.offset +import org.maplibre.compose.expressions.dsl.plus +import org.maplibre.compose.expressions.dsl.switch +import org.maplibre.compose.expressions.value.IconRotationAlignment +import org.maplibre.compose.expressions.value.SymbolAnchor +import org.maplibre.compose.layers.CircleLayer +import org.maplibre.compose.layers.SymbolLayer +import org.maplibre.compose.location.LocationClickHandler +import org.maplibre.compose.location.LocationPuckColors +import org.maplibre.compose.location.LocationPuckSizes +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.GeoJsonSource +import org.maplibre.compose.sources.rememberGeoJsonSource +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.cos +import kotlin.math.sin +import kotlin.math.sqrt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Composable +public fun LocationPuck( + idPrefix: String, + locationState: Location, + cameraState: CameraState, + oldLocationThreshold: Duration = 30.seconds, + accuracyThreshold: Float = 50f, + colors: LocationPuckColors = LocationPuckColors(), + sizes: LocationPuckSizes = LocationPuckSizes(), + showBearing: Boolean = true, + showBearingAccuracy: Boolean = true, + onClick: LocationClickHandler? = null, + onLongClick: LocationClickHandler? = null, +) { + val bearingPainter = rememberBearingPainter(sizes, colors) + val bearingAccuracyPainter = + rememberBearingAccuracyPainter( + sizes = sizes, + colors = colors, + bearingAccuracy = locationState.bearingAccuracyDegrees + ) + val locationSource = rememberLocationSource(locationState) + + CircleLayer( + id = "$idPrefix-accuracy", + source = locationSource, + visible = + accuracyThreshold <= Float.POSITIVE_INFINITY && + locationState.let { it.accuracy > accuracyThreshold }, + radius = + switch( + condition( + test = + feature["age"].asNumber() gt const(oldLocationThreshold.inWholeNanoseconds.toFloat()), + output = const(0.dp), + ), + fallback = + (feature["accuracy"].asNumber() / const(cameraState.metersPerDpAtTarget.toFloat())).dp, + ), + color = const(colors.accuracyFillColor), + strokeColor = const(colors.accuracyStrokeColor), + strokeWidth = const(sizes.accuracyStrokeWidth), + ) + + CircleLayer( + id = "$idPrefix-shadow", + source = locationSource, + visible = sizes.shadowSize > 0.dp, + radius = const(sizes.dotRadius + sizes.dotStrokeWidth + sizes.shadowSize), + color = const(colors.shadowColor), + blur = const(sizes.shadowBlur), + translate = const(DpOffset(0.dp, 1.dp)), + ) + + CircleLayer( + id = "$idPrefix-dot", + source = locationSource, + visible = true, + radius = const(sizes.dotRadius), + color = + switch( + condition( + test = + feature["age"].asNumber() gt const(oldLocationThreshold.inWholeNanoseconds.toFloat()), + output = const(colors.dotFillColorOldLocation), + ), + fallback = const(colors.dotFillColorCurrentLocation), + ), + strokeColor = const(colors.dotStrokeColor), + strokeWidth = const(sizes.dotStrokeWidth), + ) + + SymbolLayer( + id = "$idPrefix-bearing", + source = locationSource, + visible = showBearing, + iconImage = image(bearingPainter), + iconAnchor = const(SymbolAnchor.Center), + iconRotate = feature["bearing"].asNumber(const(0f)) + const(45f), + iconOffset = + offset( + -(sizes.dotRadius + sizes.dotStrokeWidth) * sqrt(2f) / 2f, + -(sizes.dotRadius + sizes.dotStrokeWidth) * sqrt(2f) / 2f, + ), + iconRotationAlignment = const(IconRotationAlignment.Map), + iconAllowOverlap = const(true), + ) + + SymbolLayer( + id = "$idPrefix-bearingAccuracy", + source = locationSource, + visible = + showBearingAccuracy, + iconImage = image(bearingAccuracyPainter), + iconAnchor = const(SymbolAnchor.Center), + iconRotate = + feature["bearing"].asNumber(const(0f)) - + const(90f) - + feature["bearingAccuracy"].asNumber(const(0f)), + iconRotationAlignment = const(IconRotationAlignment.Map), + iconAllowOverlap = const(true), + ) +} + +@Composable +private fun rememberBearingPainter( + sizes: LocationPuckSizes, + colors: LocationPuckColors, +): VectorPainter { + return rememberVectorPainter( + defaultWidth = sizes.bearingSize, + defaultHeight = sizes.bearingSize, + autoMirror = false, + ) { viewportWidth, viewportHeight -> + Path( + pathData = + PathData { + moveTo(0f, 0f) + lineTo(0f, viewportHeight) + lineTo(viewportWidth, 0f) + close() + }, + fill = SolidColor(colors.bearingColor), + ) + } +} + +@Composable +private fun rememberBearingAccuracyPainter( + sizes: LocationPuckSizes, + colors: LocationPuckColors, + bearingAccuracy: Float, +): VectorPainter { + val density by rememberUpdatedState(LocalDensity.current) + + val dotRadius by rememberUpdatedState(sizes.dotRadius) + val dotStrokeWidth by rememberUpdatedState(sizes.dotStrokeWidth) + val bearingColor by rememberUpdatedState(colors.bearingColor) + + val bearingAccuracy by rememberUpdatedState(bearingAccuracy) + + val bearingAccuracyVector by remember { + derivedStateOf { + val radius = with(density) { Offset(dotRadius.toPx(), dotRadius.toPx()) } + + val deltaDegrees = 2 * bearingAccuracy + val delta = (PI * deltaDegrees / 180.0).toFloat() + + val width = 2 * dotRadius + 2 * dotStrokeWidth + val height = 2 * dotRadius + 2 * dotStrokeWidth + + val center = with(density) { Offset((width / 2).toPx(), (height / 2).toPx()) } + + val start = center + Offset(radius.x, 0f) + val end = center + Offset(radius.x * cos(delta), radius.y * sin(delta)) + + ImageVector.Builder( + defaultWidth = width, + defaultHeight = height, + viewportWidth = with(density) { width.toPx() }, + viewportHeight = with(density) { height.toPx() }, + autoMirror = false, + ) + .apply { + path( + stroke = SolidColor(bearingColor), + strokeLineWidth = with(density) { dotStrokeWidth.toPx() }, + ) { + moveTo(start.x, start.y) + arcTo(radius.x, radius.y, 0f, delta > PI, delta > 0, end.x, end.y) + } + } + .build() + } + } + + return rememberVectorPainter(bearingAccuracyVector) +} + +@Composable +private fun rememberLocationSource(locationState: Location): GeoJsonSource { + val features = + remember(locationState) { + val location = locationState + FeatureCollection( + Feature( + geometry = Point(location.longitude, location.latitude), + properties = + buildJsonObject { + put("accuracy", location.accuracy) + put("bearing", location.bearing) + //put("bearingAccuracy", location.bearingAccuracy) + //put("age", location.timestamp.elapsedNow().inWholeNanoseconds) + }, + ) + ) + } + + return rememberGeoJsonSource(GeoJsonData.Features(features)) +} + +public typealias LocationClickHandler = (org.maplibre.compose.location.Location) -> Unit diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index 1914b37..dca58c3 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -167,14 +167,14 @@ class NavigationSession : Session() { } fun test(location: Location?) { - if (routeModel.isNavigating() && locationIndex < routeModel.polylineLocations.size) { - val loc = routeModel.polylineLocations[locationIndex] + if (routeModel.isNavigating() && locationIndex < routeModel.route.waypoints.size) { + val loc = routeModel.route.waypoints[locationIndex] val curLocation = Location(LocationManager.GPS_PROVIDER) - curLocation.longitude = loc[0] - curLocation.latitude = loc[1] + curLocation.longitude = loc[0] + 0.0003 + curLocation.latitude = loc[1] + 0.0002 update(curLocation) locationIndex += 1 - if (locationIndex > routeModel.polylineLocations.size) { + if (locationIndex > routeModel.route.waypoints.size) { val locationManager = carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager.removeUpdates(mLocationListener) @@ -185,16 +185,10 @@ class NavigationSession : Session() { } fun update(location: Location) { - surfaceRenderer.updateLocation(location) if (routeModel.isNavigating()) { routeModel.updateLocation(location) -// if (routeModel.distanceToRoute > 50) { -// routeModel.stopNavigation() -// locationIndex = 0 -// surfaceRenderer.setRouteData() -// navigationScreen.reRoute() -// } navigationScreen.updateTrip() } + surfaceRenderer.updateLocation(location) } } \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index 9295e60..8f0aa54 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -26,17 +26,17 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.kouros.navigation.car.navigation.RouteCarModel +import com.kouros.navigation.data.Constants import com.kouros.navigation.model.RouteModel -import com.kouros.navigation.utils.NavigationUtils +import com.kouros.navigation.utils.NavigationUtils.snapLocation +import com.kouros.navigation.utils.calculateZoom import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.LineLayer -import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuckColors -import org.maplibre.compose.location.rememberDefaultLocationProvider -import org.maplibre.compose.location.rememberUserLocationState +import org.maplibre.compose.location.LocationPuckSizes import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.getBaseSource @@ -63,11 +63,12 @@ class SurfaceRenderer( val previewRouteData = MutableLiveData("") - lateinit var centerLocation : Location + lateinit var centerLocation: Location var preview = false lateinit var mapView: ComposeView + var panView = false val tilt = 55.0 val padding = PaddingValues(start = 150.dp, top = 250.dp) @@ -154,8 +155,6 @@ class SurfaceRenderer( @Composable fun MapView() { - val locationProvider = rememberDefaultLocationProvider() - val locationState = rememberUserLocationState(locationProvider) val position: CameraPosition? by cameraPosition.observeAsState() val route: String? by routeData.observeAsState() val previewRoute: String? by previewRouteData.observeAsState() @@ -174,18 +173,19 @@ class SurfaceRenderer( ) MaplibreMap( cameraState = cameraState, - //baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"), - baseStyle = BaseStyle.Uri("https://kouros-online.de/liberty"), + baseStyle = BaseStyle.Uri(Constants.STYLE), ) { getBaseSource(id = "openmaptiles")?.let { tiles -> FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building") RouteLayer(route, previewRoute) } + LocationPuck( idPrefix = "user-location", - locationState = locationState, + locationState = lastLocation, cameraState = cameraState, accuracyThreshold = 10f, + sizes = LocationPuckSizes(dotRadius = 10.dp), colors = LocationPuckColors(accuracyStrokeColor = Color.Green) ) } @@ -264,6 +264,7 @@ class SurfaceRenderer( /** Handles the map zoom-in and zoom-out events. */ fun handleScale(zoomSign: Int) { synchronized(this) { + panView = true val newZoom = if (zoomSign < 0) { cameraPosition.value!!.zoom - 1.0 } else { @@ -281,27 +282,33 @@ class SurfaceRenderer( fun updateLocation(location: Location) { synchronized(this) { if (!preview) { + var snapedLocation = location var bearing: Double if (routeModel.isNavigating()) { + snapedLocation = snapLocation(location, routeModel.maneuverLocations()) bearing = routeModel.currentStep().bearing } else { bearing = cameraPosition.value!!.bearing - if (lastLocation.latitude != location.latitude) { - if (lastLocation.distanceTo(location) > 5) { - bearing = lastLocation.bearingTo(location).toDouble() + if (lastLocation.latitude != snapedLocation.latitude) { + if (lastLocation.distanceTo(snapedLocation) > 5) { + bearing = lastLocation.bearingTo(snapedLocation).toDouble() } } } - val zoom = NavigationUtils().calculateZoom(location.speed.toDouble()) + val zoom = if (!panView) { + calculateZoom(snapedLocation.speed.toDouble()) + } else { + cameraPosition.value!!.zoom + } cameraPosition.postValue( cameraPosition.value!!.copy( bearing = bearing, zoom = zoom, padding = getPaddingValues(), - target = Position(location.longitude, location.latitude), + target = Position(snapedLocation.longitude, snapedLocation.latitude), ) ) - lastLocation = location + lastLocation = snapedLocation } else { val bearing = 0.0 val zoom = 11.0 @@ -319,12 +326,13 @@ class SurfaceRenderer( fun setRouteData() { - routeData.value = routeModel.route + routeData.value = routeModel.route.routeGeoJson preview = false + panView = false } fun setPreviewRouteData(routeModel: RouteModel) { - previewRouteData.value = routeModel.route + previewRouteData.value = routeModel.route.routeGeoJson centerLocation = routeModel.centerLocation preview = true } diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt index 1fcf11c..3d82b97 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt @@ -40,8 +40,8 @@ class RouteCarModel() : RouteModel() { /** Returns the current [Step] with information such as the cue text and images. */ fun currentStep(carContext: CarContext): Step { - val maneuver = (maneuvers[maneuverIndex] as JSONObject) - val maneuverType = maneuver.getInt("type") + val maneuver = route.currentManeuver() + val maneuverType = maneuver.type val stepData = currentStep() @@ -53,9 +53,9 @@ class RouteCarModel() : RouteModel() { } when (stepData.leftDistance) { in 0.0..100.0 -> { - if (maneuverIndex < maneuvers.length()) { - val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject) - val maneuverType = maneuver.getInt("type") + if (route.currentIndex < route.maneuvers.size) { + val maneuver = route.nextManeuver() + val maneuverType = maneuver.type routing = routingData(maneuverType, carContext) } } @@ -77,22 +77,22 @@ class RouteCarModel() : RouteModel() { /** Returns the next [Step] with information such as the cue text and images. */ fun nextStep(carContext: CarContext): Step { - val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject) - val maneuverType = maneuver.getInt("type") + val maneuver = route.nextManeuver() + val maneuverType = maneuver.type val routing = routingData(maneuverType, carContext) var text = "" val distanceLeft = leftStepDistance() * 1000 when (distanceLeft) { in 0.0..100.0 -> { - if (maneuver.optJSONArray("street_names") != null) { - text = maneuver.getJSONArray("street_names").get(0) as String + if (maneuver.streetNames != null && maneuver.streetNames!!.isNotEmpty()) { + text = maneuver.streetNames!![0] } } else -> { - if (maneuver.optJSONArray("street_names") != null) { - text = maneuver.getJSONArray("street_names").get(0) as String + if (maneuver.streetNames != null && maneuver.streetNames!!.isNotEmpty()) { + text = maneuver.streetNames!![0] } } } @@ -113,40 +113,42 @@ class RouteCarModel() : RouteModel() { var type = Maneuver.TYPE_DEPART var currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change) when (routeManeuverType) { - ManeuverType.Destination.value, - ManeuverType.DestinationLeft.value, - ManeuverType.DestinationRight.value - -> { - type = Maneuver.TYPE_DESTINATION - currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_destination) - } - ManeuverType.None.value -> { type = Maneuver.TYPE_STRAIGHT currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change) } - + ManeuverType.Destination.value, + ManeuverType.DestinationRight.value, + ManeuverType.DestinationLeft.value, + -> { + type = Maneuver.TYPE_DESTINATION + currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_destination) + } ManeuverType.Right.value -> { type = Maneuver.TYPE_TURN_NORMAL_RIGHT - // currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_normal_right) - currentTurnIcon = createCarIcon(carContext, R.drawable.turn_right_48px1) + currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_normal_right) } - ManeuverType.Left.value -> { type = Maneuver.TYPE_TURN_NORMAL_LEFT currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_normal_left) } - + ManeuverType.RampRight.value -> { + type = Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT + currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_slight_right) + } ManeuverType.RampLeft.value -> { type = Maneuver.TYPE_TURN_NORMAL_LEFT currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_normal_left) } - ManeuverType.ExitRight.value -> { type = Maneuver.TYPE_TURN_SLIGHT_RIGHT currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_slight_right) } + ManeuverType.StayRight.value -> { + type = Maneuver.TYPE_KEEP_RIGHT + currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change) + } ManeuverType.StayLeft.value -> { type = Maneuver.TYPE_KEEP_LEFT currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change) diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index 322b1e2..b313d8c 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -51,7 +51,6 @@ class NavigationScreen( } override fun onGetTemplate(): Template { - // Log.i(TAG, "onGetTemplate NavigationScreen") val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() actionStripBuilder.addAction( Action.Builder() @@ -149,7 +148,7 @@ class NavigationScreen( } return RoutingInfo.Builder() .setCurrentStep( - routeModel.currentStep(carContext = carContext), + routeModel.currentStep(carContext = carContext), Distance.create(currentDistance, displayUnit) ) .setNextStep(routeModel.nextStep(carContext = carContext)) diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt index 71b2041..a27e8ce 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt @@ -4,6 +4,7 @@ import android.location.Location import android.net.Uri import android.text.Spannable import android.text.SpannableString +import android.util.Log import androidx.car.app.CarContext import androidx.car.app.CarToast import androidx.car.app.Screen @@ -22,6 +23,7 @@ import com.kouros.android.cars.carappservice.R import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.data.Constants +import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.Place import com.kouros.navigation.model.ViewModel diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt index 3a4346c..1286f6f 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt @@ -49,7 +49,6 @@ import com.kouros.navigation.data.Place import com.kouros.navigation.model.ViewModel import java.math.BigDecimal import java.math.RoundingMode -import kotlin.math.roundToInt /** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */ class RoutePreviewScreen( @@ -110,7 +109,7 @@ class RoutePreviewScreen( val itemListBuilder = ItemList.Builder() - if (routeModel.polylineLocations.isNotEmpty()) { + if (routeModel.isNavigating() && routeModel.route.waypoints.isNotEmpty()) { itemListBuilder.addItem(createRow(0, navigateAction)) } @@ -201,9 +200,8 @@ class RoutePreviewScreen( private fun createRouteText(index: Int): CarText { - val time = routeModel.routeTime - - val length = BigDecimal(routeModel.routeDistance).setScale(1, RoundingMode.HALF_EVEN) + val time = routeModel.route.summary.time + val length = BigDecimal(routeModel.route.distance).setScale(1, RoundingMode.HALF_EVEN) val firstRoute = SpannableString(" \u00b7 $length km") firstRoute.setSpan( DurationSpan.create(time.toLong()), 0, 1,0 diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt index f15f0e4..d152372 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt @@ -17,7 +17,7 @@ import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.data.Category import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Place -import com.kouros.navigation.utils.NavigationUtils.Utils.getBoundingBox +import com.kouros.navigation.utils.NavigationUtils.getBoundingBox class SearchScreen( @@ -42,8 +42,6 @@ class SearchScreen( val searchItemListBuilder = ItemList.Builder() .setNoItemsMessage("No search results to show") - - println("OnGetTemplate SearchScreen ${categories.size}") if (!isSearching) { categories.forEach { it.name @@ -111,7 +109,7 @@ class SearchScreen( geocoder.getFromLocationName( searchText, 5, - lowerLeftLat, lowerLeftLon, upperRightLat, upperRightLon + //lowerLeftLat, lowerLeftLon, upperRightLat, upperRightLon ) { for (address in it) { val name: String = address.getAddressLine(0) @@ -145,7 +143,6 @@ class SearchScreen( isSearching = false } val itemList = searchItemListBuilder.build() - println("Searching ${itemList.items.size}") invalidate() } } \ No newline at end of file diff --git a/common/data/build.gradle.kts b/common/data/build.gradle.kts index 8fd0328..0c7794d 100644 --- a/common/data/build.gradle.kts +++ b/common/data/build.gradle.kts @@ -40,21 +40,21 @@ android { } dependencies { - + implementation(libs.android.sdk.turf) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) - implementation("io.insert-koin:koin-androidx-compose:4.1.1") - implementation("io.insert-koin:koin-core:4.1.1") - implementation("io.insert-koin:koin-android:4.1.1") - implementation("io.insert-koin:koin-compose-viewmodel:4.1.1") + implementation(libs.koin.androidx.compose) + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.compose.viewmodel) // objectbox - implementation("io.objectbox:objectbox-kotlin:5.0.1") + implementation(libs.objectbox.kotlin) implementation(libs.androidx.material3) - annotationProcessor("io.objectbox:objectbox-processor:5.0.1") + annotationProcessor(libs.objectbox.processor) - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation(libs.kotlinx.serialization.json) implementation(libs.maplibre.compose) testImplementation(libs.junit) diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index d39450e..7251cc7 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -57,9 +57,8 @@ data class StepData ( var bearing: Double ) - -//val places = mutableListOf() - /* Place( +val dataPlaces = listOf( + Place( id = 0, name = "Vogelhartstr. 17", category = "Favorites", @@ -80,7 +79,7 @@ data class StepData ( city = "München", street = "Hohenwaldeckstr. 27", ) -) */ +) // GeoJSON data classes @Serializable @@ -106,7 +105,6 @@ data class Locations ( var lat : Double, var lon : Double, var street : String = "" - ) @Serializable @@ -120,6 +118,8 @@ data class ValhallaLocation ( object Constants { + const val STYLE: String = "https://kouros-online.de/liberty2" + //baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"), const val TAG: String = "Navigation" const val CONTACTS: String = "Contacts" diff --git a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt index 93eee9c..d0f7e64 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt @@ -52,7 +52,7 @@ class NavigationRepository { val route = getRoute(currentLocation, location) val routeModel = RouteModel() routeModel.startNavigation(route) - return routeModel.routeDistance + return routeModel.route.distance } fun getPlaces(): List { @@ -87,7 +87,6 @@ class NavigationRepository { ) } }) - println(url) val httpURLConnection = URL(url).openConnection() as HttpURLConnection httpURLConnection.setRequestProperty( diff --git a/common/data/src/main/java/com/kouros/navigation/data/Route.kt b/common/data/src/main/java/com/kouros/navigation/data/Route.kt new file mode 100644 index 0000000..6040c90 --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/Route.kt @@ -0,0 +1,115 @@ +package com.kouros.navigation.data + +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.createGeoJson +import com.kouros.navigation.utils.NavigationUtils.decodePolyline +import org.maplibre.geojson.Point + + +data class Route ( + /** + * A Leg is a route between only two waypoints. + * + * @since 1.0.0 + */ + val maneuvers: List, + + /** + * The distance traveled from origin to destination. + * + * @return a double number with unit meters + * @since 1.0.0 + */ + val distance: Double, + + /** + * List of [List] objects. Each `waypoint` 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 + */ + var waypoints: List>, + + val pointLocations : List, + + val summary: Summary, + + val trip: Trip, + + val time: Double, + + var routingManeuvers : List, + + var routeGeoJson : String, + + var currentIndex: Int + +) { + + class Builder { + private lateinit var maneuvers: List + private var distance: Double = 0.0 + + private var time: Double = 0.0 + private lateinit var waypoints: List> + private lateinit var pointLocations: List + + private lateinit var summary : Summary + + private lateinit var trip : Trip + + private lateinit var routingManeuvers: List + + private var routeGeoJson = "" + + fun route (route: String ) = apply { + if (route.isNotEmpty() && route != "[]") { + val gson = GsonBuilder().serializeNulls().create() + val valhalla = gson.fromJson(route, ValhallaJson::class.java) + trip = valhalla.trip + } + } + + fun build(): Route { + maneuvers = trip.legs[0].maneuvers + summary = trip.summary + distance = summary.length + time = summary.time + waypoints = decodePolyline(trip.legs[0].shape) + val points = mutableListOf() + for (loc in waypoints) { + val point = Point.fromLngLat(loc[0], loc[1]) + points.add(point) + } + pointLocations = points + val routings = mutableListOf() + for (maneuver in maneuvers) { + routings.add(maneuver) + } + this.routingManeuvers = routings + this.routeGeoJson = createGeoJson(waypoints) + return Route( + maneuvers, distance, waypoints, pointLocations, summary, trip, time, routingManeuvers, routeGeoJson, 0 + ) + } + } + + fun clear() { + waypoints = mutableListOf() + routingManeuvers = mutableListOf() + routeGeoJson = "" + } + + fun currentManeuver() : Maneuvers { + return maneuvers[currentIndex] + } + + fun nextManeuver() : Maneuvers { + return maneuvers[currentIndex+1] + } +} \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/valhalla/Legs.kt b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Legs.kt new file mode 100644 index 0000000..dd3ffcf --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Legs.kt @@ -0,0 +1,17 @@ +package com.kouros.navigation.data.valhalla + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonIgnoreUnknownKeys + + +@OptIn(ExperimentalSerializationApi::class) +@JsonIgnoreUnknownKeys +data class Legs ( + + @SerializedName("maneuvers" ) var maneuvers : ArrayList = arrayListOf(), + @SerializedName("summary" ) var summary : Summary = Summary(), + @SerializedName("shape" ) var shape : String = "" + +) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/valhalla/Locations.kt b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Locations.kt new file mode 100644 index 0000000..8dc017e --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Locations.kt @@ -0,0 +1,19 @@ +package com.kouros.navigation.data.valhalla + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonIgnoreUnknownKeys + + +@OptIn(ExperimentalSerializationApi::class) +@JsonIgnoreUnknownKeys +data class Locations ( + + @SerializedName("type" ) var type : String = "", + @SerializedName("lat" ) var lat : Double = 0.0, + @SerializedName("lon" ) var lon : Double = 0.0, + @SerializedName("side_of_street" ) var sideOfStreet : String = "", + @SerializedName("original_index" ) var originalIndex : Int = 0 + +) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/valhalla/Maneuvers.kt b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Maneuvers.kt new file mode 100644 index 0000000..10262c1 --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Maneuvers.kt @@ -0,0 +1,29 @@ +package com.kouros.navigation.data.valhalla + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonIgnoreUnknownKeys + + +@OptIn(ExperimentalSerializationApi::class) +@JsonIgnoreUnknownKeys +data class Maneuvers( + + @SerializedName("begin_shape_index") var beginShapeIndex: Int, + @SerializedName("end_shape_index") var endShapeIndex: Int, + @SerializedName("type") var type: Int = 0, + @SerializedName("instruction") var instruction: String = "", + @SerializedName("verbal_succinct_transition_instruction") var verbalSuccinctTransitionInstruction: String = "", + @SerializedName("verbal_pre_transition_instruction") var verbalPreTransitionInstruction: String = "", + @SerializedName("verbal_post_transition_instruction") var verbalPostTransitionInstruction: String = "", + @SerializedName("street_names") val streetNames: List? = arrayListOf(), + @SerializedName("bearing_after") var bearingAfter: Int = 0, + @SerializedName("time") var time: Double = 0.0, + @SerializedName("length") var length: Double = 0.0, + @SerializedName("cost") var cost: Double = 0.0, + @SerializedName("verbal_multi_cue") var verbalMultiCue: Boolean = false, + @SerializedName("travel_mode") var travelMode: String = "", + @SerializedName("travel_type") var travelType: String = "", + + ) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/valhalla/Summary.kt b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Summary.kt new file mode 100644 index 0000000..0f83d4a --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Summary.kt @@ -0,0 +1,25 @@ +package com.kouros.navigation.data.valhalla + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonIgnoreUnknownKeys + + +@OptIn(ExperimentalSerializationApi::class) +@JsonIgnoreUnknownKeys +data class Summary ( + + @SerializedName("has_time_restrictions" ) var hasTimeRestrictions : Boolean = false, + @SerializedName("has_toll" ) var hasToll : Boolean = false, + @SerializedName("has_highway" ) var hasHighway : Boolean = false, + @SerializedName("has_ferry" ) var hasFerry : Boolean = false, + @SerializedName("min_lat" ) var minLat : Double = 0.0, + @SerializedName("min_lon" ) var minLon : Double = 0.0, + @SerializedName("max_lat" ) var maxLat : Double = 0.0, + @SerializedName("max_lon" ) var maxLon : Double = 0.0, + @SerializedName("time" ) var time : Double = 0.0, + @SerializedName("length" ) var length : Double = 0.0, + @SerializedName("cost" ) var cost : Double = 0.0 + +) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/valhalla/Trip.kt b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Trip.kt new file mode 100644 index 0000000..0709a7c --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/valhalla/Trip.kt @@ -0,0 +1,21 @@ +package com.kouros.navigation.data.valhalla + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonIgnoreUnknownKeys + + +@OptIn(ExperimentalSerializationApi::class) +@JsonIgnoreUnknownKeys +data class Trip ( + + @SerializedName("locations" ) var locations : ArrayList = arrayListOf(), + @SerializedName("legs" ) var legs : ArrayList = arrayListOf(), + @SerializedName("summary" ) var summary : Summary = Summary(), + @SerializedName("status_message" ) var statusMessage : String = "", + @SerializedName("status" ) var status : Int = 0, + @SerializedName("units" ) var units : String = "", + @SerializedName("language" ) var language : String = "", + +) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/valhalla/ValhallaJson.kt b/common/data/src/main/java/com/kouros/navigation/data/valhalla/ValhallaJson.kt new file mode 100644 index 0000000..8b1b7ee --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/valhalla/ValhallaJson.kt @@ -0,0 +1,16 @@ +package com.kouros.navigation.data.valhalla + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonIgnoreUnknownKeys + + +@OptIn(ExperimentalSerializationApi::class) +@JsonIgnoreUnknownKeys +data class ValhallaJson ( + + @SerializedName("trip" ) var trip : Trip = Trip(), + @SerializedName("id" ) var id : String = "" + +) diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt index 2081732..ad82a3f 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -2,32 +2,29 @@ package com.kouros.navigation.model import android.location.Location import android.location.LocationManager +import com.kouros.navigation.data.Constants.homeLocation import com.kouros.navigation.data.Place +import com.kouros.navigation.data.Route import com.kouros.navigation.data.StepData -import com.kouros.navigation.utils.NavigationUtils -import com.kouros.navigation.utils.NavigationUtils.Utils.createGeoJson -import com.kouros.navigation.utils.NavigationUtils.Utils.decodePolyline -import org.json.JSONArray -import org.json.JSONObject +import com.kouros.navigation.utils.location +import org.maplibre.geojson.Point import kotlin.math.absoluteValue import kotlin.math.roundToInt -open class RouteModel () { - var polylineLocations: List> = emptyList() - lateinit var maneuvers: JSONArray - lateinit var locations: JSONArray - private lateinit var summary: JSONObject - var routeDistance = 0.0 - - var routeTime = 0.0 - +open class RouteModel() { lateinit var centerLocation: Location + lateinit var destination: Place var navigating = false + var arrived = false - var maneuverIndex = 0 + var maneuverType = 0 + + /* + Index in a maneuver + */ var currentIndex = 0 var distanceToStepEnd = 0F @@ -38,53 +35,27 @@ open class RouteModel () { var endIndex = 0 - var routingManeuvers = mutableListOf() - - var route = "" - - data class Builder( - var route: String? = null, - var fromLocation: Location? = null, - var toLocation: Location? = null) { - - fun route(route: String) = apply { this.route = route } - fun fromLocation(fromLocation: Location) = apply { this.fromLocation = fromLocation } - fun toLocation(toLocation: Location) = apply { this.toLocation = toLocation } - //fun build() = RouteModel(route!!, fromLocation!!, toLocation!!) - } - private fun decodeValhallaRoute(valhallaRoute: String) { - if (valhallaRoute.isEmpty() || valhallaRoute == "[]") { - return; - } - val jObject = JSONObject(valhallaRoute) - val trip = jObject.getJSONObject("trip") - locations = trip.getJSONArray("locations") - val legs = trip.getJSONArray("legs") - summary = trip.getJSONObject("summary") - routeTime = summary.getDouble("time") - routeDistance = summary.getDouble("length") - centerLocation = createCenterLocation() - maneuvers = legs.getJSONObject(0).getJSONArray("maneuvers") - val shape = legs.getJSONObject(0).getString("shape") - polylineLocations = decodePolyline(shape) - } - - private fun createCenterLocation() : Location { - val latitude = summary.getDouble("max_lat") - (summary.getDouble("max_lat") - summary.getDouble("min_lat")) - val longitude = summary.getDouble("max_lon") - (summary.getDouble("max_lon") - summary.getDouble("min_lon")) - return NavigationUtils().location(latitude, longitude) - } + lateinit var route: Route fun startNavigation(valhallaRoute: String) { - decodeValhallaRoute(valhallaRoute) - for (i in 0.. { - if (maneuverIndex < maneuvers.length()) { - val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject) - if (maneuver.optJSONArray("street_names") != null) { - text = maneuver.getJSONArray("street_names").get(0) as String + if (route.currentIndex < route.maneuvers.size) { + val maneuver = route.nextManeuver() + if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) { + text = maneuver.streetNames[0] } } } } return StepData(text, distanceStepLeft, bearing.toDouble()) - } /** Calculates the index in a maneuver. */ @@ -143,8 +111,8 @@ open class RouteModel () { var nearestLocation = 100000.0f for (i in beginShapeIndex..endShapeIndex) { val polylineLocation = Location(LocationManager.GPS_PROVIDER) - polylineLocation.longitude = polylineLocations[i][0] - polylineLocation.latitude = polylineLocations[i][1] + polylineLocation.longitude = route.waypoints[i][0] + polylineLocation.latitude = route.waypoints[i][1] val distance: Float = location.distanceTo(polylineLocation) if (distance < nearestLocation) { nearestLocation = distance @@ -154,16 +122,16 @@ open class RouteModel () { distanceToStepEnd = 0F val loc1 = Location(LocationManager.GPS_PROVIDER) val loc2 = Location(LocationManager.GPS_PROVIDER) - loc1.longitude = polylineLocations[i][0] - loc1.latitude = polylineLocations[i][1] - loc2.longitude = polylineLocations[i+1][0] - loc2.latitude = polylineLocations[i+1][1] + loc1.longitude = route.waypoints[i][0] + loc1.latitude = route.waypoints[i][1] + loc2.longitude = route.waypoints[i + 1][0] + loc2.latitude = route.waypoints[i + 1][1] bearing = loc1.bearingTo(loc2).absoluteValue for (j in i + 1..endShapeIndex) { - loc1.longitude = polylineLocations[j - 1][0] - loc1.latitude = polylineLocations[j - 1][1] - loc2.longitude = polylineLocations[j][0] - loc2.latitude = polylineLocations[j][1] + 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] distanceToStepEnd += loc1.distanceTo(loc2) } } @@ -178,8 +146,8 @@ open class RouteModel () { var nearestLocation = 100000.0f for (i in beginShapeIndex..endShapeIndex) { val polylineLocation = Location(LocationManager.GPS_PROVIDER) - polylineLocation.longitude = polylineLocations[i][0] - polylineLocation.latitude = polylineLocations[i][1] + polylineLocation.longitude = route.waypoints[i][0] + polylineLocation.latitude = route.waypoints[i][1] val distance: Float = location.distanceTo(polylineLocation) if (distance < nearestLocation) { nearestLocation = distance @@ -188,15 +156,21 @@ open class RouteModel () { return nearestLocation } + fun maneuverLocations(): List { + val beginShapeIndex = route.currentManeuver().beginShapeIndex + val endShapeIndex = route.currentManeuver().endShapeIndex + return route.pointLocations.subList(beginShapeIndex, endShapeIndex) + } + fun travelLeftTime(): Double { var timeLeft = 0.0 - for (i in maneuverIndex + 1.. 0) { - val maneuver = routingManeuvers[maneuverIndex] - val curTime = maneuver.getDouble("time") + val maneuver = route.currentManeuver() + val curTime = maneuver.time val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex) val time = curTime * percent / 100 timeLeft += time @@ -206,8 +180,8 @@ open class RouteModel () { /** Returns the current [Step] left distance in km. */ fun leftStepDistance(): Double { - val maneuver = routingManeuvers[maneuverIndex] - var leftDistance = maneuver.getDouble("length") + val maneuver = route.routingManeuvers[route.currentIndex] + var leftDistance = maneuver.length if (endIndex > 0) { leftDistance = (distanceToStepEnd / 1000).toDouble() } @@ -216,13 +190,13 @@ open class RouteModel () { fun travelLeftDistance(): Double { var leftDistance = 0.0 - for (i in maneuverIndex + 1.. 0) { - val maneuver = routingManeuvers[maneuverIndex] - val curDistance = maneuver.getDouble("length") + val maneuver = route.routingManeuvers[route.currentIndex] + val curDistance = maneuver.length val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex) val time = curDistance * percent / 100 leftDistance += time @@ -239,11 +213,9 @@ open class RouteModel () { } fun stopNavigation() { + route.clear() navigating = false - polylineLocations = mutableListOf() - routingManeuvers = mutableListOf() - route = "" - maneuverIndex = 0 + //maneuverIndex = 0 currentIndex = 0 distanceToStepEnd = 0F beginIndex = 0 diff --git a/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt index 958aeab..3a4bc9e 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt @@ -13,6 +13,7 @@ import com.kouros.navigation.data.ObjectBox.boxStore import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place_ import com.kouros.navigation.utils.NavigationUtils +import com.kouros.navigation.utils.location import io.objectbox.kotlin.boxFor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -43,7 +44,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() { val placeBox = boxStore.boxFor(Place::class) pl.addAll(placeBox.all) for (place in pl) { - val plLocation = NavigationUtils().location(place.latitude, place.longitude) + val plLocation = location(place.latitude, place.longitude) val distance = repository.getRouteDistance(location, plLocation) place.distance = distance.toFloat() } @@ -87,7 +88,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() { address.address, 5) { for (adr in it) { if (addressLines.size > 1) { - val plLocation = NavigationUtils().location(adr.latitude, adr.longitude) + val plLocation = location(adr.latitude, adr.longitude) val distance = repository.getRouteDistance(currentLocation, plLocation) contactList.add( Place( diff --git a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt index 826cd29..a4d1081 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt @@ -6,6 +6,13 @@ 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.Point +import org.maplibre.turf.TurfClassification +import org.maplibre.turf.TurfConversion +import org.maplibre.turf.TurfJoins +import org.maplibre.turf.TurfMeta +import org.maplibre.turf.TurfMisc +import org.maplibre.turf.TurfTransformation import java.lang.Math.toDegrees import java.lang.Math.toRadians import kotlin.math.asin @@ -14,118 +21,130 @@ import kotlin.math.cos import kotlin.math.pow import kotlin.math.sin -class NavigationUtils() { - object Utils { - fun decodePolyline(encoded: String, vararg precisionOptional: Int): List> { - val precision = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6 - val factor = 10.0.pow(precision) - var lat = 0 - var lng = 0 - val coordinates = mutableListOf>() - var index = 0 - while (index < encoded.length) { - var byte = 0x20 - var shift = 0 - var result = 0 - while (byte >= 0x20) { - byte = encoded[index].code - 63 - result = result or ((byte and 0x1f) shl shift) - shift += 5 - index++ - } - lat += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) +object NavigationUtils { - byte = 0x20 - shift = 0 - result = 0 - while (byte >= 0x20) { - byte = encoded[index].code - 63 - result = result or ((byte and 0x1f) shl shift) - shift += 5 - index++ - } - lng += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) - coordinates.add(listOf(lng.toDouble() / factor, lat.toDouble() / factor)) - } - - return coordinates + fun snapLocation(location: Location, stepCoordinates: List): Location { + val oldPoint = Point.fromLngLat(location.longitude, location.latitude) + if (stepCoordinates.size > 1) { + val pointFeature = TurfMisc.nearestPointOnLine(oldPoint, stepCoordinates) + val point = pointFeature.geometry() as Point + location.latitude = point.latitude() + location.longitude = point.longitude() } - - fun createGeoJson(lineCoordinates: List>): String { - val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates) - val feature = GeoJsonFeature(type = "Feature", geometry = lineString) - val featureCollection = - GeoJsonFeatureCollection(type = "FeatureCollection", features = listOf(feature)) - val jsonString = Json.Default.encodeToString(featureCollection) - return jsonString - } - - fun getBoundingBox( - lat: Double, - lon: Double, - radius: Double - ): Map> { - val earthRadius = 6371.0 - val maxLat = lat + Math.toDegrees(radius / earthRadius) - val minLat = lat - Math.toDegrees(radius / earthRadius) - val maxLon = lon + Math.toDegrees(radius / earthRadius / cos(Math.toRadians(lat))) - val minLon = lon - Math.toDegrees(radius / earthRadius / cos(Math.toRadians(lat))) - - return mapOf( - "nw" to mapOf("lat" to maxLat, "lon" to minLon), - "ne" to mapOf("lat" to maxLat, "lon" to maxLon), - "sw" to mapOf("lat" to minLat, "lon" to minLon), - "se" to mapOf("lat" to minLat, "lon" to maxLon) - ) - } - - fun computeOffset(from: Location, distance: Double, heading: Double): Location { - val earthRadius = 6371009.0 - var distance = distance - var heading = heading - distance /= earthRadius - heading = toRadians(heading) - val fromLat: Double = toRadians(from.latitude) - val fromLng: Double = toRadians(from.longitude) - val cosDistance: Double = cos(distance) - val sinDistance = sin(distance) - val sinFromLat = sin(fromLat) - val cosFromLat: Double = cos(fromLat) - val sinLat: Double = cosDistance * sinFromLat + sinDistance * cosFromLat * cos(heading) - val dLng: Double = atan2( - sinDistance * cosFromLat * sin(heading), - cosDistance - sinFromLat * sinLat - ) - val snap = Location(LocationManager.GPS_PROVIDER) - snap.latitude = toDegrees(asin(sinLat)) - snap.longitude = toDegrees(fromLng + dLng) - return snap - //return LatLng(toDegrees(asin(sinLat)), toDegrees(fromLng + dLng)) - } - } - - fun calculateZoom(speed: Double?): Double { - if (speed == null) { - return 18.0 - } - val zoom = when (speed.toInt()) { - in 0..10 -> 17.0 - in 11..20 -> 17.0 - in 21..30 -> 17.0 - in 31..40 -> 16.0 - in 41..50 -> 15.0 - in 51..60 -> 14.0 - else -> 11 - } - return zoom.toDouble() - } - - fun location(latitude: Double, longitude: Double): Location { - val location = Location(LocationManager.GPS_PROVIDER) - location.longitude = longitude - location.latitude = latitude return location } + + fun decodePolyline(encoded: String, vararg precisionOptional: Int): List> { + val precision = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6 + val factor = 10.0.pow(precision) + + var lat = 0 + var lng = 0 + val coordinates = mutableListOf>() + var index = 0 + + while (index < encoded.length) { + var byte = 0x20 + var shift = 0 + var result = 0 + while (byte >= 0x20) { + byte = encoded[index].code - 63 + result = result or ((byte and 0x1f) shl shift) + shift += 5 + index++ + } + lat += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) + + byte = 0x20 + shift = 0 + result = 0 + while (byte >= 0x20) { + byte = encoded[index].code - 63 + result = result or ((byte and 0x1f) shl shift) + shift += 5 + index++ + } + lng += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) + coordinates.add(listOf(lng.toDouble() / factor, lat.toDouble() / factor)) + } + + return coordinates + } + + fun createGeoJson(lineCoordinates: List>): String { + val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates) + val feature = GeoJsonFeature(type = "Feature", geometry = lineString) + val featureCollection = + GeoJsonFeatureCollection(type = "FeatureCollection", features = listOf(feature)) + val jsonString = Json.encodeToString(featureCollection) + return jsonString + } + + fun getBoundingBox( + lat: Double, + lon: Double, + radius: Double + ): Map> { + val earthRadius = 6371.0 + val maxLat = lat + Math.toDegrees(radius / earthRadius) + val minLat = lat - Math.toDegrees(radius / earthRadius) + val maxLon = lon + Math.toDegrees(radius / earthRadius / cos(Math.toRadians(lat))) + val minLon = lon - Math.toDegrees(radius / earthRadius / cos(Math.toRadians(lat))) + + return mapOf( + "nw" to mapOf("lat" to maxLat, "lon" to minLon), + "ne" to mapOf("lat" to maxLat, "lon" to maxLon), + "sw" to mapOf("lat" to minLat, "lon" to minLon), + "se" to mapOf("lat" to minLat, "lon" to maxLon) + ) + } + + fun computeOffset(from: Location, distance: Double, heading: Double): Location { + val earthRadius = 6371009.0 + var distance = distance + var heading = heading + distance /= earthRadius + heading = toRadians(heading) + val fromLat: Double = toRadians(from.latitude) + val fromLng: Double = toRadians(from.longitude) + val cosDistance: Double = cos(distance) + val sinDistance = sin(distance) + val sinFromLat = sin(fromLat) + val cosFromLat: Double = cos(fromLat) + val sinLat: Double = cosDistance * sinFromLat + sinDistance * cosFromLat * cos(heading) + val dLng: Double = atan2( + sinDistance * cosFromLat * sin(heading), + cosDistance - sinFromLat * sinLat + ) + val snap = Location(LocationManager.GPS_PROVIDER) + snap.latitude = toDegrees(asin(sinLat)) + snap.longitude = toDegrees(fromLng + dLng) + return snap + //return LatLng(toDegrees(asin(sinLat)), toDegrees(fromLng + dLng)) + } +} + +fun calculateZoom(speed: Double?): Double { + if (speed == null) { + return 18.0 + } + val zoom = when (speed.toInt()) { + in 0..10 -> 17.0 + in 11..20 -> 17.0 + in 21..30 -> 17.0 + in 31..40 -> 16.0 + in 41..50 -> 15.0 + in 51..60 -> 14.0 + else -> 11 + } + return zoom.toDouble() +} + +fun location(latitude: Double, longitude: Double): Location { + val location = Location(LocationManager.GPS_PROVIDER) + location.longitude = longitude + location.latitude = latitude + return location } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8133e9b..8a5bff2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,24 @@ [versions] agp = "8.13.1" +androidSdkTurf = "6.0.1" gradle = "8.13.1" +koinAndroid = "4.1.1" koinAndroidxCompose = "4.1.1" +koinComposeViewmodel = "4.1.1" +koinCore = "4.1.1" kotlin = "2.2.21" coreKtx = "1.17.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" +kotlinxSerializationJson = "1.9.0" lifecycleRuntimeKtx = "2.9.4" composeBom = "2025.11.00" appcompat = "1.7.1" material = "1.13.0" carApp = "1.7.0" -#objectboxKotlin = "5.0.1" +objectboxKotlin = "5.0.1" +objectboxProcessor = "5.0.1" ui = "1.9.4" material3 = "1.4.0" runtimeLivedata = "1.9.4" @@ -24,6 +30,7 @@ runtime = "1.9.4" accompanist = "0.32.0" [libraries] +android-sdk-turf = { module = "org.maplibre.gl:android-sdk-turf", version.ref = "androidSdkTurf" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -33,10 +40,16 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroidxCompose" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeViewmodel" } +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" } #objectbox-kotlin = { module = "io.objectbox:objectbox-kotlin", version.ref = "objectboxKotlin" } +objectbox-kotlin = { module = "io.objectbox:objectbox-kotlin", version.ref = "objectboxKotlin" } +objectbox-processor = { module = "io.objectbox:objectbox-processor", version.ref = "objectboxProcessor" } ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" } maplibre-composeMaterial3 = { module = "org.maplibre.compose:maplibre-compose-material3", version = "maplibre-composeMaterial3" }