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

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

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?) {
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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