Serialize Json

This commit is contained in:
Dimitris
2025-11-20 10:27:33 +01:00
parent 3f3bdeb96d
commit 33f5ef4f34
24 changed files with 919 additions and 391 deletions

View File

@@ -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 package com.kouros.navigation
import android.Manifest import android.Manifest
@@ -21,7 +5,6 @@ import android.annotation.SuppressLint
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@@ -47,7 +30,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -68,14 +51,16 @@ import androidx.lifecycle.Observer
import com.example.places.ui.theme.PlacesTheme import com.example.places.ui.theme.PlacesTheme
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel 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 kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.maplibre.compose.camera.CameraPosition 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.expressions.dsl.const
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors 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.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState 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.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.GeoJsonData
import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.sources.rememberGeoJsonSource
@@ -99,24 +84,24 @@ import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
val routeData = MutableLiveData("")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
val routeData = MutableLiveData("")
val vieModel = ViewModel(NavigationRepository()) val vieModel = ViewModel(NavigationRepository())
val routeModel = RouteModel() val routeModel = RouteModel()
var tilt = 0.0 var tilt = 50.0
val curLocation = Location(LocationManager.GPS_PROVIDER)
val instruction: MutableLiveData<StepData> by lazy { val instruction: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>() MutableLiveData<StepData>()
} }
var lastLocation = Location(LocationManager.GPS_PROVIDER)
val observer = Observer<String> { newRoute -> val observer = Observer<String> { newRoute ->
routeModel.startNavigation(newRoute) routeModel.startNavigation(newRoute)
routeData.value = routeModel.route routeData.value = routeModel.route.routeGeoJson
} }
val cameraPosition = MutableLiveData( val cameraPosition = MutableLiveData(
@@ -126,40 +111,14 @@ class MainActivity : ComponentActivity() {
) )
) )
var locationIndex = 0
var test = false
init { init {
vieModel.route.observe(this, observer) 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..<routeModel.polylineLocations.size) {
val loc = routeModel.polylineLocations[i]
val curLocation = Location(LocationManager.GPS_PROVIDER)
curLocation.longitude = loc[0]
curLocation.latitude = loc[1]
routeModel.updateLocation(curLocation)
val leftTime = routeModel.travelLeftTime()
val leftDistance = routeModel.travelLeftDistance()
Log.i(TAG, " leftTime: ${leftTime / 60}")
Log.i(TAG, " leftDistance: $leftDistance")
Log.i(TAG, "Cue: ${routeModel.maneuvers[routeModel.maneuverIndex]}")
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -198,10 +157,10 @@ class MainActivity : ComponentActivity() {
scope.launch { scope.launch {
snackbarHostState.showSnackbar("Starte Navigation") snackbarHostState.showSnackbar("Starte Navigation")
} }
if (!routeModel.isNavigating()) { if (!routeModel.isNavigating() && lastLocation.latitude != 0.0) {
tilt = 60.0 tilt = 60.0
vieModel.loadRoute( vieModel.loadRoute(
curLocation, lastLocation,
Constants.home2Location Constants.home2Location
) )
} else { } else {
@@ -271,8 +230,8 @@ class MainActivity : ComponentActivity() {
Card { Card {
Column { Column {
Icon( Icon(
painter = painterResource(com.kouros.android.cars.carappservice.R.drawable.ic_turn_normal_right), painter = painterResource(R.drawable.ic_turn_normal_right),
contentDescription = stringResource(id = com.kouros.android.cars.carappservice.R.string.accept_action_title) contentDescription = stringResource(id = R.string.accept_action_title)
) )
if (step != null) { if (step != null) {
Text(text = step.bearing.toString(), fontSize = 25.sp) Text(text = step.bearing.toString(), fontSize = 25.sp)
@@ -295,10 +254,23 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
fun MapView() { fun MapView() {
val locationProvider = rememberDefaultLocationProvider() val locationProvider = rememberDefaultLocationProvider(
val locationState = rememberUserLocationState(locationProvider) updateInterval = 0.1.seconds,
updateLocation(locationState.location) desiredAccuracy = DesiredAccuracy.Highest
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
if (!test) {
updateLocation(locationState.value)
} else {
test()
}
if (locationState.value != null && lastLocation.latitude == 0.0) {
lastLocation.latitude = locationState.value?.position!!.latitude
lastLocation.longitude = locationState.value?.position!!.longitude
}
val position: CameraPosition? by cameraPosition.observeAsState() val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState() val route: String? by routeData.observeAsState()
val cameraState = val cameraState =
rememberCameraState( rememberCameraState(
@@ -311,42 +283,32 @@ class MainActivity : ComponentActivity() {
zoom = 15.0, zoom = 15.0,
) )
) )
if (locationState.location != null) {
curLocation.latitude = locationState.location?.position!!.latitude
curLocation.longitude = locationState.location?.position!!.longitude
}
MaplibreMap( MaplibreMap(
cameraState = cameraState, cameraState = cameraState,
//baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"), baseStyle = BaseStyle.Uri(Constants.STYLE),
baseStyle = BaseStyle.Uri("https://kouros-online.de/liberty"),
options =
MapOptions(
gestureOptions = GestureOptions(
isTiltEnabled = true,
isZoomEnabled = true,
isRotateEnabled = false,
isScrollEnabled = true,
),
ornamentOptions = OrnamentOptions(
isScaleBarEnabled = false
)
)
) { ) {
LocationPuck(
idPrefix = "user-location",
locationState = locationState,
cameraState = cameraState,
accuracyThreshold = 10f,
colors = LocationPuckColors(accuracyStrokeColor = Color.Green)
)
getBaseSource(id = "openmaptiles")?.let { tiles -> getBaseSource(id = "openmaptiles")?.let { tiles ->
FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building") FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building")
RouteLayer(route) 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( cameraState.animateTo(
finalPosition = CameraPosition( finalPosition = CameraPosition(
bearing = position!!.bearing, bearing = position!!.bearing,
@@ -354,9 +316,22 @@ class MainActivity : ComponentActivity() {
target = position!!.target, target = position!!.target,
tilt = tilt 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 @Composable
@@ -368,17 +343,66 @@ class MainActivity : ComponentActivity() {
id = "routes-casing", id = "routes-casing",
source = routes, source = routes,
color = const(Color.White), color = const(Color.White),
width = const(6.dp), width = const(10.dp),
) )
LineLayer( LineLayer(
id = "routes", id = "routes",
source = routes, source = routes,
color = const(Color.Blue), 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 @Composable
fun PlaceList(viewModel: ViewModel = koinViewModel()) { fun PlaceList(viewModel: ViewModel = koinViewModel()) {
var categories: List<Category> var categories: List<Category>

View File

@@ -45,6 +45,7 @@ dependencies {
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
//implementation(libs.maplibre.composeMaterial3) //implementation(libs.maplibre.composeMaterial3)
implementation(project(":common:data")) implementation(project(":common:data"))
implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.runtime.livedata)
implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.foundation)

View File

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

View File

@@ -167,14 +167,14 @@ class NavigationSession : Session() {
} }
fun test(location: Location?) { fun test(location: Location?) {
if (routeModel.isNavigating() && locationIndex < routeModel.polylineLocations.size) { if (routeModel.isNavigating() && locationIndex < routeModel.route.waypoints.size) {
val loc = routeModel.polylineLocations[locationIndex] val loc = routeModel.route.waypoints[locationIndex]
val curLocation = Location(LocationManager.GPS_PROVIDER) val curLocation = Location(LocationManager.GPS_PROVIDER)
curLocation.longitude = loc[0] curLocation.longitude = loc[0] + 0.0003
curLocation.latitude = loc[1] curLocation.latitude = loc[1] + 0.0002
update(curLocation) update(curLocation)
locationIndex += 1 locationIndex += 1
if (locationIndex > routeModel.polylineLocations.size) { if (locationIndex > routeModel.route.waypoints.size) {
val locationManager = val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.removeUpdates(mLocationListener) locationManager.removeUpdates(mLocationListener)
@@ -185,16 +185,10 @@ class NavigationSession : Session() {
} }
fun update(location: Location) { fun update(location: Location) {
surfaceRenderer.updateLocation(location)
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
routeModel.updateLocation(location) routeModel.updateLocation(location)
// if (routeModel.distanceToRoute > 50) {
// routeModel.stopNavigation()
// locationIndex = 0
// surfaceRenderer.setRouteData()
// navigationScreen.reRoute()
// }
navigationScreen.updateTrip() navigationScreen.updateTrip()
} }
surfaceRenderer.updateLocation(location)
} }
} }

View File

@@ -26,17 +26,17 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.model.RouteModel 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.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.sources.GeoJsonData import org.maplibre.compose.sources.GeoJsonData
import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.getBaseSource
@@ -63,11 +63,12 @@ class SurfaceRenderer(
val previewRouteData = MutableLiveData("") val previewRouteData = MutableLiveData("")
lateinit var centerLocation : Location lateinit var centerLocation: Location
var preview = false var preview = false
lateinit var mapView: ComposeView lateinit var mapView: ComposeView
var panView = false
val tilt = 55.0 val tilt = 55.0
val padding = PaddingValues(start = 150.dp, top = 250.dp) val padding = PaddingValues(start = 150.dp, top = 250.dp)
@@ -154,8 +155,6 @@ class SurfaceRenderer(
@Composable @Composable
fun MapView() { fun MapView() {
val locationProvider = rememberDefaultLocationProvider()
val locationState = rememberUserLocationState(locationProvider)
val position: CameraPosition? by cameraPosition.observeAsState() val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState() val route: String? by routeData.observeAsState()
val previewRoute: String? by previewRouteData.observeAsState() val previewRoute: String? by previewRouteData.observeAsState()
@@ -174,18 +173,19 @@ class SurfaceRenderer(
) )
MaplibreMap( MaplibreMap(
cameraState = cameraState, cameraState = cameraState,
//baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"), baseStyle = BaseStyle.Uri(Constants.STYLE),
baseStyle = BaseStyle.Uri("https://kouros-online.de/liberty"),
) { ) {
getBaseSource(id = "openmaptiles")?.let { tiles -> getBaseSource(id = "openmaptiles")?.let { tiles ->
FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building") FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building")
RouteLayer(route, previewRoute) RouteLayer(route, previewRoute)
} }
LocationPuck( LocationPuck(
idPrefix = "user-location", idPrefix = "user-location",
locationState = locationState, locationState = lastLocation,
cameraState = cameraState, cameraState = cameraState,
accuracyThreshold = 10f, accuracyThreshold = 10f,
sizes = LocationPuckSizes(dotRadius = 10.dp),
colors = LocationPuckColors(accuracyStrokeColor = Color.Green) colors = LocationPuckColors(accuracyStrokeColor = Color.Green)
) )
} }
@@ -264,6 +264,7 @@ class SurfaceRenderer(
/** Handles the map zoom-in and zoom-out events. */ /** Handles the map zoom-in and zoom-out events. */
fun handleScale(zoomSign: Int) { fun handleScale(zoomSign: Int) {
synchronized(this) { synchronized(this) {
panView = true
val newZoom = if (zoomSign < 0) { val newZoom = if (zoomSign < 0) {
cameraPosition.value!!.zoom - 1.0 cameraPosition.value!!.zoom - 1.0
} else { } else {
@@ -281,27 +282,33 @@ class SurfaceRenderer(
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
synchronized(this) { synchronized(this) {
if (!preview) { if (!preview) {
var snapedLocation = location
var bearing: Double var bearing: Double
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
snapedLocation = snapLocation(location, routeModel.maneuverLocations())
bearing = routeModel.currentStep().bearing bearing = routeModel.currentStep().bearing
} else { } else {
bearing = cameraPosition.value!!.bearing bearing = cameraPosition.value!!.bearing
if (lastLocation.latitude != location.latitude) { if (lastLocation.latitude != snapedLocation.latitude) {
if (lastLocation.distanceTo(location) > 5) { if (lastLocation.distanceTo(snapedLocation) > 5) {
bearing = lastLocation.bearingTo(location).toDouble() 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.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
bearing = bearing, bearing = bearing,
zoom = zoom, zoom = zoom,
padding = getPaddingValues(), padding = getPaddingValues(),
target = Position(location.longitude, location.latitude), target = Position(snapedLocation.longitude, snapedLocation.latitude),
) )
) )
lastLocation = location lastLocation = snapedLocation
} else { } else {
val bearing = 0.0 val bearing = 0.0
val zoom = 11.0 val zoom = 11.0
@@ -319,12 +326,13 @@ class SurfaceRenderer(
fun setRouteData() { fun setRouteData() {
routeData.value = routeModel.route routeData.value = routeModel.route.routeGeoJson
preview = false preview = false
panView = false
} }
fun setPreviewRouteData(routeModel: RouteModel) { fun setPreviewRouteData(routeModel: RouteModel) {
previewRouteData.value = routeModel.route previewRouteData.value = routeModel.route.routeGeoJson
centerLocation = routeModel.centerLocation centerLocation = routeModel.centerLocation
preview = true preview = true
} }

View File

@@ -40,8 +40,8 @@ class RouteCarModel() : RouteModel() {
/** Returns the current [Step] with information such as the cue text and images. */ /** Returns the current [Step] with information such as the cue text and images. */
fun currentStep(carContext: CarContext): Step { fun currentStep(carContext: CarContext): Step {
val maneuver = (maneuvers[maneuverIndex] as JSONObject) val maneuver = route.currentManeuver()
val maneuverType = maneuver.getInt("type") val maneuverType = maneuver.type
val stepData = currentStep() val stepData = currentStep()
@@ -53,9 +53,9 @@ class RouteCarModel() : RouteModel() {
} }
when (stepData.leftDistance) { when (stepData.leftDistance) {
in 0.0..100.0 -> { in 0.0..100.0 -> {
if (maneuverIndex < maneuvers.length()) { if (route.currentIndex < route.maneuvers.size) {
val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject) val maneuver = route.nextManeuver()
val maneuverType = maneuver.getInt("type") val maneuverType = maneuver.type
routing = routingData(maneuverType, carContext) routing = routingData(maneuverType, carContext)
} }
} }
@@ -77,22 +77,22 @@ class RouteCarModel() : RouteModel() {
/** Returns the next [Step] with information such as the cue text and images. */ /** Returns the next [Step] with information such as the cue text and images. */
fun nextStep(carContext: CarContext): Step { fun nextStep(carContext: CarContext): Step {
val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject) val maneuver = route.nextManeuver()
val maneuverType = maneuver.getInt("type") val maneuverType = maneuver.type
val routing = routingData(maneuverType, carContext) val routing = routingData(maneuverType, carContext)
var text = "" var text = ""
val distanceLeft = leftStepDistance() * 1000 val distanceLeft = leftStepDistance() * 1000
when (distanceLeft) { when (distanceLeft) {
in 0.0..100.0 -> { in 0.0..100.0 -> {
if (maneuver.optJSONArray("street_names") != null) { if (maneuver.streetNames != null && maneuver.streetNames!!.isNotEmpty()) {
text = maneuver.getJSONArray("street_names").get(0) as String text = maneuver.streetNames!![0]
} }
} }
else -> { else -> {
if (maneuver.optJSONArray("street_names") != null) { if (maneuver.streetNames != null && maneuver.streetNames!!.isNotEmpty()) {
text = maneuver.getJSONArray("street_names").get(0) as String text = maneuver.streetNames!![0]
} }
} }
} }
@@ -113,40 +113,42 @@ class RouteCarModel() : RouteModel() {
var type = Maneuver.TYPE_DEPART var type = Maneuver.TYPE_DEPART
var currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change) var currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change)
when (routeManeuverType) { 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 -> { ManeuverType.None.value -> {
type = Maneuver.TYPE_STRAIGHT type = Maneuver.TYPE_STRAIGHT
currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change) 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 -> { ManeuverType.Right.value -> {
type = Maneuver.TYPE_TURN_NORMAL_RIGHT type = Maneuver.TYPE_TURN_NORMAL_RIGHT
// currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_normal_right) currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_normal_right)
currentTurnIcon = createCarIcon(carContext, R.drawable.turn_right_48px1)
} }
ManeuverType.Left.value -> { ManeuverType.Left.value -> {
type = Maneuver.TYPE_TURN_NORMAL_LEFT type = Maneuver.TYPE_TURN_NORMAL_LEFT
currentTurnIcon = createCarIcon(carContext, R.drawable.ic_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 -> { ManeuverType.RampLeft.value -> {
type = Maneuver.TYPE_TURN_NORMAL_LEFT type = Maneuver.TYPE_TURN_NORMAL_LEFT
currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_normal_left) currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_normal_left)
} }
ManeuverType.ExitRight.value -> { ManeuverType.ExitRight.value -> {
type = Maneuver.TYPE_TURN_SLIGHT_RIGHT type = Maneuver.TYPE_TURN_SLIGHT_RIGHT
currentTurnIcon = createCarIcon(carContext, R.drawable.ic_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 -> { ManeuverType.StayLeft.value -> {
type = Maneuver.TYPE_KEEP_LEFT type = Maneuver.TYPE_KEEP_LEFT
currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change) currentTurnIcon = createCarIcon(carContext, R.drawable.ic_turn_name_change)

View File

@@ -51,7 +51,6 @@ class NavigationScreen(
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
// Log.i(TAG, "onGetTemplate NavigationScreen")
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction( actionStripBuilder.addAction(
Action.Builder() Action.Builder()
@@ -149,7 +148,7 @@ class NavigationScreen(
} }
return RoutingInfo.Builder() return RoutingInfo.Builder()
.setCurrentStep( .setCurrentStep(
routeModel.currentStep(carContext = carContext), routeModel.currentStep(carContext = carContext),
Distance.create(currentDistance, displayUnit) Distance.create(currentDistance, displayUnit)
) )
.setNextStep(routeModel.nextStep(carContext = carContext)) .setNextStep(routeModel.nextStep(carContext = carContext))

View File

@@ -4,6 +4,7 @@ import android.location.Location
import android.net.Uri import android.net.Uri
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen 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.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel

View File

@@ -49,7 +49,6 @@ import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import kotlin.math.roundToInt
/** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */ /** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */
class RoutePreviewScreen( class RoutePreviewScreen(
@@ -110,7 +109,7 @@ class RoutePreviewScreen(
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
if (routeModel.polylineLocations.isNotEmpty()) { if (routeModel.isNavigating() && routeModel.route.waypoints.isNotEmpty()) {
itemListBuilder.addItem(createRow(0, navigateAction)) itemListBuilder.addItem(createRow(0, navigateAction))
} }
@@ -201,9 +200,8 @@ class RoutePreviewScreen(
private fun createRouteText(index: Int): CarText { private fun createRouteText(index: Int): CarText {
val time = routeModel.routeTime val time = routeModel.route.summary.time
val length = BigDecimal(routeModel.route.distance).setScale(1, RoundingMode.HALF_EVEN)
val length = BigDecimal(routeModel.routeDistance).setScale(1, RoundingMode.HALF_EVEN)
val firstRoute = SpannableString(" \u00b7 $length km") val firstRoute = SpannableString(" \u00b7 $length km")
firstRoute.setSpan( firstRoute.setSpan(
DurationSpan.create(time.toLong()), 0, 1,0 DurationSpan.create(time.toLong()), 0, 1,0

View File

@@ -17,7 +17,7 @@ import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.utils.NavigationUtils.Utils.getBoundingBox import com.kouros.navigation.utils.NavigationUtils.getBoundingBox
class SearchScreen( class SearchScreen(
@@ -42,8 +42,6 @@ class SearchScreen(
val searchItemListBuilder = ItemList.Builder() val searchItemListBuilder = ItemList.Builder()
.setNoItemsMessage("No search results to show") .setNoItemsMessage("No search results to show")
println("OnGetTemplate SearchScreen ${categories.size}")
if (!isSearching) { if (!isSearching) {
categories.forEach { categories.forEach {
it.name it.name
@@ -111,7 +109,7 @@ class SearchScreen(
geocoder.getFromLocationName( geocoder.getFromLocationName(
searchText, 5, searchText, 5,
lowerLeftLat, lowerLeftLon, upperRightLat, upperRightLon //lowerLeftLat, lowerLeftLon, upperRightLat, upperRightLon
) { ) {
for (address in it) { for (address in it) {
val name: String = address.getAddressLine(0) val name: String = address.getAddressLine(0)
@@ -145,7 +143,6 @@ class SearchScreen(
isSearching = false isSearching = false
} }
val itemList = searchItemListBuilder.build() val itemList = searchItemListBuilder.build()
println("Searching ${itemList.items.size}")
invalidate() invalidate()
} }
} }

View File

@@ -40,21 +40,21 @@ android {
} }
dependencies { dependencies {
implementation(libs.android.sdk.turf)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material) implementation(libs.material)
implementation("io.insert-koin:koin-androidx-compose:4.1.1") implementation(libs.koin.androidx.compose)
implementation("io.insert-koin:koin-core:4.1.1") implementation(libs.koin.core)
implementation("io.insert-koin:koin-android:4.1.1") implementation(libs.koin.android)
implementation("io.insert-koin:koin-compose-viewmodel:4.1.1") implementation(libs.koin.compose.viewmodel)
// objectbox // objectbox
implementation("io.objectbox:objectbox-kotlin:5.0.1") implementation(libs.objectbox.kotlin)
implementation(libs.androidx.material3) 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) implementation(libs.maplibre.compose)
testImplementation(libs.junit) testImplementation(libs.junit)

View File

@@ -57,9 +57,8 @@ data class StepData (
var bearing: Double var bearing: Double
) )
val dataPlaces = listOf(
//val places = mutableListOf<Place>() Place(
/* Place(
id = 0, id = 0,
name = "Vogelhartstr. 17", name = "Vogelhartstr. 17",
category = "Favorites", category = "Favorites",
@@ -80,7 +79,7 @@ data class StepData (
city = "München", city = "München",
street = "Hohenwaldeckstr. 27", street = "Hohenwaldeckstr. 27",
) )
) */ )
// GeoJSON data classes // GeoJSON data classes
@Serializable @Serializable
@@ -106,7 +105,6 @@ data class Locations (
var lat : Double, var lat : Double,
var lon : Double, var lon : Double,
var street : String = "" var street : String = ""
) )
@Serializable @Serializable
@@ -120,6 +118,8 @@ data class ValhallaLocation (
object Constants { 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 TAG: String = "Navigation"
const val CONTACTS: String = "Contacts" const val CONTACTS: String = "Contacts"

View File

@@ -52,7 +52,7 @@ class NavigationRepository {
val route = getRoute(currentLocation, location) val route = getRoute(currentLocation, location)
val routeModel = RouteModel() val routeModel = RouteModel()
routeModel.startNavigation(route) routeModel.startNavigation(route)
return routeModel.routeDistance return routeModel.route.distance
} }
fun getPlaces(): List<Place> { fun getPlaces(): List<Place> {
@@ -87,7 +87,6 @@ class NavigationRepository {
) )
} }
}) })
println(url) println(url)
val httpURLConnection = URL(url).openConnection() as HttpURLConnection val httpURLConnection = URL(url).openConnection() as HttpURLConnection
httpURLConnection.setRequestProperty( httpURLConnection.setRequestProperty(

View File

@@ -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<Maneuvers>,
/**
* The distance traveled from origin to destination.
*
* @return a double number with unit meters
* @since 1.0.0
*/
val distance: Double,
/**
* List of [List<Double>] 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<List<Double>>,
val pointLocations : List<Point>,
val summary: Summary,
val trip: Trip,
val time: Double,
var routingManeuvers : List<Maneuvers>,
var routeGeoJson : String,
var currentIndex: Int
) {
class Builder {
private lateinit var maneuvers: List<Maneuvers>
private var distance: Double = 0.0
private var time: Double = 0.0
private lateinit var waypoints: List<List<Double>>
private lateinit var pointLocations: List<Point>
private lateinit var summary : Summary
private lateinit var trip : Trip
private lateinit var routingManeuvers: List<Maneuvers>
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<Point>()
for (loc in waypoints) {
val point = Point.fromLngLat(loc[0], loc[1])
points.add(point)
}
pointLocations = points
val routings = mutableListOf<Maneuvers>()
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]
}
}

View File

@@ -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<Maneuvers> = arrayListOf(),
@SerializedName("summary" ) var summary : Summary = Summary(),
@SerializedName("shape" ) var shape : String = ""
)

View File

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

View File

@@ -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<String>? = 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 = "",
)

View File

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

View File

@@ -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<Locations> = arrayListOf(),
@SerializedName("legs" ) var legs : ArrayList<Legs> = 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 = "",
)

View File

@@ -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 = ""
)

View File

@@ -2,32 +2,29 @@ package com.kouros.navigation.model
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.NavigationUtils import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.NavigationUtils.Utils.createGeoJson import org.maplibre.geojson.Point
import com.kouros.navigation.utils.NavigationUtils.Utils.decodePolyline
import org.json.JSONArray
import org.json.JSONObject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
open class RouteModel () { open class RouteModel() {
var polylineLocations: List<List<Double>> = emptyList()
lateinit var maneuvers: JSONArray
lateinit var locations: JSONArray
private lateinit var summary: JSONObject
var routeDistance = 0.0
var routeTime = 0.0
lateinit var centerLocation: Location lateinit var centerLocation: Location
lateinit var destination: Place lateinit var destination: Place
var navigating = false var navigating = false
var arrived = false var arrived = false
var maneuverIndex = 0
var maneuverType = 0 var maneuverType = 0
/*
Index in a maneuver
*/
var currentIndex = 0 var currentIndex = 0
var distanceToStepEnd = 0F var distanceToStepEnd = 0F
@@ -38,53 +35,27 @@ open class RouteModel () {
var endIndex = 0 var endIndex = 0
var routingManeuvers = mutableListOf<JSONObject>() lateinit var route: Route
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)
}
fun startNavigation(valhallaRoute: String) { fun startNavigation(valhallaRoute: String) {
decodeValhallaRoute(valhallaRoute) route = Route.Builder()
for (i in 0..<maneuvers.length()) { .route(valhallaRoute)
val maneuver = (maneuvers[i] as JSONObject) .build()
routingManeuvers.add(maneuver) centerLocation = createCenterLocation()
}
route = createGeoJson(polylineLocations)
navigating = true navigating = true
} }
private fun createCenterLocation(): Location {
if (route.summary.maxLat == 0.0) {
return location(homeLocation.latitude, homeLocation.longitude)
}
val latitude =
route.summary.maxLat - (route.summary.maxLat - route.summary.minLat)
val longitude =
route.summary.maxLon - (route.summary.maxLon - route.summary.minLon)
return location(latitude, longitude)
}
val currentDistance: Double val currentDistance: Double
/** Returns the current [Step] with information such as the cue text and images. */ /** Returns the current [Step] with information such as the cue text and images. */
get() { get() {
@@ -93,45 +64,42 @@ open class RouteModel () {
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
var nearestDistance = 100000.0f var nearestDistance = 100000.0f
maneuverIndex = -1 route.currentIndex = -1
// find maneuver // find maneuver
for (i in 0..<maneuvers.length()) { for ((i, maneuver) in route.maneuvers.withIndex()) {
val maneuver = (maneuvers[i] as JSONObject) val beginShapeIndex = maneuver.beginShapeIndex
val beginShapeIndex = maneuver.getString("begin_shape_index").toInt() val endShapeIndex = maneuver.endShapeIndex
val endShapeIndex = maneuver.getString("end_shape_index").toInt()
val distance = calculateDistance(beginShapeIndex, endShapeIndex, location) val distance = calculateDistance(beginShapeIndex, endShapeIndex, location)
if (distance < nearestDistance) { if (distance < nearestDistance) {
nearestDistance = distance nearestDistance = distance
maneuverIndex = i route.currentIndex = i
calculateCurrentIndex(beginShapeIndex, endShapeIndex, location) calculateCurrentIndex(beginShapeIndex, endShapeIndex, location)
} }
} }
} }
fun currentStep(): StepData { fun currentStep(): StepData {
val maneuver = (maneuvers[maneuverIndex] as JSONObject) val maneuver = route.currentManeuver()
var text = "" var text = ""
if (maneuver.optJSONArray("street_names") != null) { println("Maneuver $maneuver")
text = maneuver.getJSONArray("street_names").get(0) as String if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
} }
if (bearing == 0F) { if (bearing == 0F) {
if (maneuver.has("bearing_after")) { bearing = maneuver.bearingAfter.toFloat()
bearing = maneuver.getInt("bearing_after").toFloat()
}
} }
val distanceStepLeft = leftStepDistance() * 1000 val distanceStepLeft = leftStepDistance() * 1000
when (distanceStepLeft) { when (distanceStepLeft) {
in 0.0..100.0 -> { in 0.0..100.0 -> {
if (maneuverIndex < maneuvers.length()) { if (route.currentIndex < route.maneuvers.size) {
val maneuver = (maneuvers[maneuverIndex + 1] as JSONObject) val maneuver = route.nextManeuver()
if (maneuver.optJSONArray("street_names") != null) { if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.getJSONArray("street_names").get(0) as String text = maneuver.streetNames[0]
} }
} }
} }
} }
return StepData(text, distanceStepLeft, bearing.toDouble()) return StepData(text, distanceStepLeft, bearing.toDouble())
} }
/** Calculates the index in a maneuver. */ /** Calculates the index in a maneuver. */
@@ -143,8 +111,8 @@ open class RouteModel () {
var nearestLocation = 100000.0f var nearestLocation = 100000.0f
for (i in beginShapeIndex..endShapeIndex) { for (i in beginShapeIndex..endShapeIndex) {
val polylineLocation = Location(LocationManager.GPS_PROVIDER) val polylineLocation = Location(LocationManager.GPS_PROVIDER)
polylineLocation.longitude = polylineLocations[i][0] polylineLocation.longitude = route.waypoints[i][0]
polylineLocation.latitude = polylineLocations[i][1] polylineLocation.latitude = route.waypoints[i][1]
val distance: Float = location.distanceTo(polylineLocation) val distance: Float = location.distanceTo(polylineLocation)
if (distance < nearestLocation) { if (distance < nearestLocation) {
nearestLocation = distance nearestLocation = distance
@@ -154,16 +122,16 @@ open class RouteModel () {
distanceToStepEnd = 0F distanceToStepEnd = 0F
val loc1 = Location(LocationManager.GPS_PROVIDER) val loc1 = Location(LocationManager.GPS_PROVIDER)
val loc2 = Location(LocationManager.GPS_PROVIDER) val loc2 = Location(LocationManager.GPS_PROVIDER)
loc1.longitude = polylineLocations[i][0] loc1.longitude = route.waypoints[i][0]
loc1.latitude = polylineLocations[i][1] loc1.latitude = route.waypoints[i][1]
loc2.longitude = polylineLocations[i+1][0] loc2.longitude = route.waypoints[i + 1][0]
loc2.latitude = polylineLocations[i+1][1] loc2.latitude = route.waypoints[i + 1][1]
bearing = loc1.bearingTo(loc2).absoluteValue bearing = loc1.bearingTo(loc2).absoluteValue
for (j in i + 1..endShapeIndex) { for (j in i + 1..endShapeIndex) {
loc1.longitude = polylineLocations[j - 1][0] loc1.longitude = route.waypoints[j - 1][0]
loc1.latitude = polylineLocations[j - 1][1] loc1.latitude = route.waypoints[j - 1][1]
loc2.longitude = polylineLocations[j][0] loc2.longitude = route.waypoints[j][0]
loc2.latitude = polylineLocations[j][1] loc2.latitude = route.waypoints[j][1]
distanceToStepEnd += loc1.distanceTo(loc2) distanceToStepEnd += loc1.distanceTo(loc2)
} }
} }
@@ -178,8 +146,8 @@ open class RouteModel () {
var nearestLocation = 100000.0f var nearestLocation = 100000.0f
for (i in beginShapeIndex..endShapeIndex) { for (i in beginShapeIndex..endShapeIndex) {
val polylineLocation = Location(LocationManager.GPS_PROVIDER) val polylineLocation = Location(LocationManager.GPS_PROVIDER)
polylineLocation.longitude = polylineLocations[i][0] polylineLocation.longitude = route.waypoints[i][0]
polylineLocation.latitude = polylineLocations[i][1] polylineLocation.latitude = route.waypoints[i][1]
val distance: Float = location.distanceTo(polylineLocation) val distance: Float = location.distanceTo(polylineLocation)
if (distance < nearestLocation) { if (distance < nearestLocation) {
nearestLocation = distance nearestLocation = distance
@@ -188,15 +156,21 @@ open class RouteModel () {
return nearestLocation return nearestLocation
} }
fun maneuverLocations(): List<Point> {
val beginShapeIndex = route.currentManeuver().beginShapeIndex
val endShapeIndex = route.currentManeuver().endShapeIndex
return route.pointLocations.subList(beginShapeIndex, endShapeIndex)
}
fun travelLeftTime(): Double { fun travelLeftTime(): Double {
var timeLeft = 0.0 var timeLeft = 0.0
for (i in maneuverIndex + 1..<routingManeuvers.size) { for (i in route.currentIndex + 1..<route.routingManeuvers.size) {
val maneuver = routingManeuvers[i] val maneuver = route.routingManeuvers[i]
timeLeft += maneuver.getDouble("time") timeLeft += maneuver.time
} }
if (endIndex > 0) { if (endIndex > 0) {
val maneuver = routingManeuvers[maneuverIndex] val maneuver = route.currentManeuver()
val curTime = maneuver.getDouble("time") val curTime = maneuver.time
val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex) val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex)
val time = curTime * percent / 100 val time = curTime * percent / 100
timeLeft += time timeLeft += time
@@ -206,8 +180,8 @@ open class RouteModel () {
/** Returns the current [Step] left distance in km. */ /** Returns the current [Step] left distance in km. */
fun leftStepDistance(): Double { fun leftStepDistance(): Double {
val maneuver = routingManeuvers[maneuverIndex] val maneuver = route.routingManeuvers[route.currentIndex]
var leftDistance = maneuver.getDouble("length") var leftDistance = maneuver.length
if (endIndex > 0) { if (endIndex > 0) {
leftDistance = (distanceToStepEnd / 1000).toDouble() leftDistance = (distanceToStepEnd / 1000).toDouble()
} }
@@ -216,13 +190,13 @@ open class RouteModel () {
fun travelLeftDistance(): Double { fun travelLeftDistance(): Double {
var leftDistance = 0.0 var leftDistance = 0.0
for (i in maneuverIndex + 1..<routingManeuvers.size) { for (i in route.currentIndex + 1..<route.routingManeuvers.size) {
val maneuver = routingManeuvers[i] val maneuver = route.routingManeuvers[i]
leftDistance += maneuver.getDouble("length") leftDistance += maneuver.length
} }
if (endIndex > 0) { if (endIndex > 0) {
val maneuver = routingManeuvers[maneuverIndex] val maneuver = route.routingManeuvers[route.currentIndex]
val curDistance = maneuver.getDouble("length") val curDistance = maneuver.length
val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex) val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex)
val time = curDistance * percent / 100 val time = curDistance * percent / 100
leftDistance += time leftDistance += time
@@ -239,11 +213,9 @@ open class RouteModel () {
} }
fun stopNavigation() { fun stopNavigation() {
route.clear()
navigating = false navigating = false
polylineLocations = mutableListOf() //maneuverIndex = 0
routingManeuvers = mutableListOf()
route = ""
maneuverIndex = 0
currentIndex = 0 currentIndex = 0
distanceToStepEnd = 0F distanceToStepEnd = 0F
beginIndex = 0 beginIndex = 0

View File

@@ -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.data.Place_ import com.kouros.navigation.data.Place_
import com.kouros.navigation.utils.NavigationUtils import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.location
import io.objectbox.kotlin.boxFor import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -43,7 +44,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
pl.addAll(placeBox.all) pl.addAll(placeBox.all)
for (place in pl) { 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) val distance = repository.getRouteDistance(location, plLocation)
place.distance = distance.toFloat() place.distance = distance.toFloat()
} }
@@ -87,7 +88,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
address.address, 5) { address.address, 5) {
for (adr in it) { for (adr in it) {
if (addressLines.size > 1) { 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) val distance = repository.getRouteDistance(currentLocation, plLocation)
contactList.add( contactList.add(
Place( Place(

View File

@@ -6,6 +6,13 @@ import com.kouros.navigation.data.GeoJsonFeature
import com.kouros.navigation.data.GeoJsonFeatureCollection import com.kouros.navigation.data.GeoJsonFeatureCollection
import com.kouros.navigation.data.GeoJsonLineString import com.kouros.navigation.data.GeoJsonLineString
import kotlinx.serialization.json.Json 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.toDegrees
import java.lang.Math.toRadians import java.lang.Math.toRadians
import kotlin.math.asin import kotlin.math.asin
@@ -14,118 +21,130 @@ import kotlin.math.cos
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sin import kotlin.math.sin
class NavigationUtils() {
object Utils {
fun decodePolyline(encoded: String, vararg precisionOptional: Int): List<List<Double>> {
val precision = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6
val factor = 10.0.pow(precision)
var lat = 0
var lng = 0
val coordinates = mutableListOf<List<Double>>()
var index = 0
while (index < encoded.length) { object NavigationUtils {
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 fun snapLocation(location: Location, stepCoordinates: List<Point>): Location {
shift = 0 val oldPoint = Point.fromLngLat(location.longitude, location.latitude)
result = 0 if (stepCoordinates.size > 1) {
while (byte >= 0x20) { val pointFeature = TurfMisc.nearestPointOnLine(oldPoint, stepCoordinates)
byte = encoded[index].code - 63 val point = pointFeature.geometry() as Point
result = result or ((byte and 0x1f) shl shift) location.latitude = point.latitude()
shift += 5 location.longitude = point.longitude()
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<List<Double>>): 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<String, Map<String, Double>> {
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 return location
} }
fun decodePolyline(encoded: String, vararg precisionOptional: Int): List<List<Double>> {
val precision = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6
val factor = 10.0.pow(precision)
var lat = 0
var lng = 0
val coordinates = mutableListOf<List<Double>>()
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<List<Double>>): 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<String, Map<String, Double>> {
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
} }

View File

@@ -1,18 +1,24 @@
[versions] [versions]
agp = "8.13.1" agp = "8.13.1"
androidSdkTurf = "6.0.1"
gradle = "8.13.1" gradle = "8.13.1"
koinAndroid = "4.1.1"
koinAndroidxCompose = "4.1.1" koinAndroidxCompose = "4.1.1"
koinComposeViewmodel = "4.1.1"
koinCore = "4.1.1"
kotlin = "2.2.21" kotlin = "2.2.21"
coreKtx = "1.17.0" coreKtx = "1.17.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
kotlinxSerializationJson = "1.9.0"
lifecycleRuntimeKtx = "2.9.4" lifecycleRuntimeKtx = "2.9.4"
composeBom = "2025.11.00" composeBom = "2025.11.00"
appcompat = "1.7.1" appcompat = "1.7.1"
material = "1.13.0" material = "1.13.0"
carApp = "1.7.0" carApp = "1.7.0"
#objectboxKotlin = "5.0.1" objectboxKotlin = "5.0.1"
objectboxProcessor = "5.0.1"
ui = "1.9.4" ui = "1.9.4"
material3 = "1.4.0" material3 = "1.4.0"
runtimeLivedata = "1.9.4" runtimeLivedata = "1.9.4"
@@ -24,6 +30,7 @@ runtime = "1.9.4"
accompanist = "0.32.0" accompanist = "0.32.0"
[libraries] [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" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
junit = { group = "junit", name = "junit", version.ref = "junit" } 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-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 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-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" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" } 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-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" } ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" } maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" }
maplibre-composeMaterial3 = { module = "org.maplibre.compose:maplibre-compose-material3", version = "maplibre-composeMaterial3" } maplibre-composeMaterial3 = { module = "org.maplibre.compose:maplibre-compose-material3", version = "maplibre-composeMaterial3" }