From b9030dbc50c12633d23e0bff2c345026521a2b13 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Wed, 17 Dec 2025 13:07:29 +0100 Subject: [PATCH] Amenities GeoUtils --- .../navigation/car/NavigationSession.kt | 2 +- .../kouros/navigation/car/SurfaceRenderer.kt | 48 ++---- .../com/kouros/navigation/car/map/MapView.kt | 9 +- .../navigation/car/screen/CategoriesScreen.kt | 2 +- .../navigation/car/screen/CategoryScreen.kt | 9 +- .../java/com/kouros/navigation/data/Data.kt | 20 --- .../java/com/kouros/navigation/data/Route.kt | 11 +- .../navigation/data/overpass/Overpass.kt | 6 +- .../com/kouros/navigation/utils/GeoUtils.kt | 153 ++++++++++++++++++ .../navigation/utils/NavigationUtils.kt | 135 ++-------------- 10 files changed, 198 insertions(+), 197 deletions(-) create mode 100644 common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index 8954b69..888f50b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -26,7 +26,7 @@ import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.ObjectBox -import com.kouros.navigation.utils.NavigationUtils.snapLocation +import com.kouros.navigation.utils.GeoUtils.snapLocation class NavigationSession : Session(), NavigationScreen.Listener { diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index b52c9ff..ff0eeca 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -37,6 +37,7 @@ import com.kouros.navigation.data.Constants import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.model.RouteModel import com.kouros.navigation.utils.bearing +import com.kouros.navigation.utils.calcTilt import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.duration import com.kouros.navigation.utils.location @@ -188,36 +189,17 @@ class SurfaceRenderer( ) { val cameraDuration = duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing) - var bearing = position.bearing - var zoom = position.zoom - var target = position.target - var localTilt = tilt - val currentSpeed: Float? by speed.observeAsState() - when (viewStyle) { - ViewStyle.VIEW -> { - DrawImage(paddingValues, currentSpeed, width, height) - } - - ViewStyle.PREVIEW -> { - bearing = 0.0 - zoom = previewZoom(previewDistance) - target = Position(centerLocation.longitude, centerLocation.latitude) - localTilt = 0.0 - } - - else -> { - bearing = 0.0 - localTilt = 0.0 - zoom = 12.0 - } + val currentSpeed: Float? by speed.observeAsState() + if (viewStyle == ViewStyle.VIEW) { + DrawImage(paddingValues, currentSpeed, width, height) } LaunchedEffect(position, viewStyle) { cameraState.animateTo( finalPosition = CameraPosition( - bearing = bearing, - zoom = zoom, - target = target, - tilt = localTilt, + bearing = position.bearing, + zoom = position.zoom, + target = position.target, + tilt = tilt, padding = paddingValues ), duration = cameraDuration @@ -235,21 +217,15 @@ class SurfaceRenderer( /** Handles the map zoom-in and zoom-out events. */ fun handleScale(zoomSign: Int) { synchronized(this) { - viewStyle = ViewStyle.PAN_VIEW + if (viewStyle == ViewStyle.VIEW) { + viewStyle = ViewStyle.PAN_VIEW + } val newZoom = if (zoomSign < 0) { cameraPosition.value!!.zoom - 1.0 } else { cameraPosition.value!!.zoom + 1.0 } - tilt = if (newZoom < 13) { - 0.0 - } else { - if (tilt == 0.0) { - 55.0 - } else { - tilt - } - } + tilt = calcTilt(newZoom, tilt) updateCameraPosition( cameraPosition.value!!.bearing, newZoom, diff --git a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt index 9d1c431..8f1fd6b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt @@ -112,7 +112,6 @@ fun MapLibre( @Composable fun RouteLayer(routeData: String?, viewStyle: ViewStyle) { if (routeData != null && routeData.isNotEmpty()) { - println(routeData) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) if (viewStyle == ViewStyle.VIEW) { LineLayer( @@ -149,10 +148,10 @@ fun RouteLayer(routeData: String?, viewStyle: ViewStyle) { source = routes, // Convert a drawable resource to a MapLibre image // drawAsSdf = true allows us to tint the image programmatically - iconImage = image(painterResource(R.drawable.ic_place_white_24dp), drawAsSdf = true), + iconImage = image(painterResource(com.kouros.android.cars.carappservice.R.drawable.ev_station_24px), drawAsSdf = true), // Now we can apply any color we want! - iconColor = const(Color.Blue), - iconSize = const(1.5f) + iconColor = const(Color.Red), + iconSize = const(5.0f) ) } } @@ -283,7 +282,7 @@ fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues { return when (viewStyle) { ViewStyle.VIEW -> PaddingValues(start = 50.dp, top = distanceFromTop(height).dp) ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp) - else -> PaddingValues(start = 450.dp, bottom = 0.dp) + else -> PaddingValues(start = 250.dp, bottom = 0.dp) } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt index 0397564..21b2bc4 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt @@ -68,7 +68,7 @@ class CategoriesScreen( val header = Header.Builder() .setStartHeaderAction(Action.BACK) - .setTitle("title") + .setTitle(carContext.getString(R.string.category_title)) .build() return ListTemplate.Builder() diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt index 8d06906..22540ca 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt @@ -23,11 +23,10 @@ import androidx.lifecycle.Observer import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.navigation.NavigationMessage -import com.kouros.navigation.data.Constants.homeLocation import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.model.ViewModel -import com.kouros.navigation.utils.NavigationUtils.createGeoJson +import com.kouros.navigation.utils.GeoUtils.createPointCollection import com.kouros.navigation.utils.location import kotlin.math.min @@ -50,11 +49,10 @@ class CategoryScreen( loc.longitude = it.lon!! loc.latitude = it.lat!! } - if (coordinates.isEmpty()) - coordinates.add(listOf(location.longitude, location.latitude)) + coordinates.add(listOf(it.lon!!, it.lat!!)) } if (elements.isNotEmpty()) { - val route = createGeoJson("Point", coordinates) + val route = createPointCollection(coordinates) surfaceRenderer.setCategories(loc, route) invalidate() } @@ -106,7 +104,6 @@ class CategoryScreen( .setStartHeaderAction(Action.BACK) .setTitle(carContext.getString(R.string.charging_station)) .build() - val builder = MapWithContentTemplate.Builder() .setContentTemplate( ListTemplate.Builder() diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index 2baf84f..f6f7e6a 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -67,26 +67,6 @@ data class StepData ( ) -// GeoJSON data classes - -@Serializable -data class GeoJsonType( - val type: String, - val coordinates: List> -) - -@Serializable -data class GeoJsonFeature( - val type: String, - val geometry: GeoJsonType, -) - -@Serializable -data class GeoJsonFeatureCollection( - val type: String, - val features: List -) - @Serializable data class Locations ( var lat : Double, diff --git a/common/data/src/main/java/com/kouros/navigation/data/Route.kt b/common/data/src/main/java/com/kouros/navigation/data/Route.kt index 1958932..657d92c 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Route.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Route.kt @@ -6,14 +6,11 @@ import com.kouros.navigation.data.valhalla.Maneuvers import com.kouros.navigation.data.valhalla.Summary import com.kouros.navigation.data.valhalla.Trip import com.kouros.navigation.data.valhalla.ValhallaJson -import com.kouros.navigation.utils.NavigationUtils.createCenterLocation -import com.kouros.navigation.utils.NavigationUtils.createGeoJson -import com.kouros.navigation.utils.NavigationUtils.decodePolyline +import com.kouros.navigation.utils.GeoUtils.createCenterLocation +import com.kouros.navigation.utils.GeoUtils.createLineStringCollection +import com.kouros.navigation.utils.GeoUtils.decodePolyline import com.kouros.navigation.utils.location -import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.Point -import org.maplibre.turf.TurfMeasurement -import kotlin.math.roundToInt data class Route( @@ -95,7 +92,7 @@ data class Route( points.add(point) } pointLocations = points - routeGeoJson = createGeoJson("LineString", waypoints) + routeGeoJson = createLineStringCollection( waypoints) centerLocation = createCenterLocation(routeGeoJson) return Route( maneuvers, diff --git a/common/data/src/main/java/com/kouros/navigation/data/overpass/Overpass.kt b/common/data/src/main/java/com/kouros/navigation/data/overpass/Overpass.kt index 4f73aa8..61b47cb 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/overpass/Overpass.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/overpass/Overpass.kt @@ -2,6 +2,8 @@ package com.kouros.navigation.data.overpass import android.location.Location import com.google.gson.GsonBuilder +import com.kouros.navigation.utils.GeoUtils.getBoundingBox2 +import com.kouros.navigation.utils.GeoUtils.getOverpassBbox import com.kouros.navigation.utils.NavigationUtils import java.io.OutputStreamWriter import java.net.HttpURLConnection @@ -11,8 +13,8 @@ class Overpass { val overpassUrl = "https://overpass.kumi.systems/api/interpreter" fun getAmenities(category: String, location: Location) : List { - val boundingBox = NavigationUtils.getOverpassBbox(location, 2.0) - val bb = NavigationUtils.getBoundingBox2(location, 2.0) + val boundingBox = getOverpassBbox(location, 2.0) + val bb = getBoundingBox2(location, 2.0) val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection httpURLConnection.requestMethod = "POST" httpURLConnection.setRequestProperty( diff --git a/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt new file mode 100644 index 0000000..e032ad7 --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt @@ -0,0 +1,153 @@ +package com.kouros.navigation.utils + +import android.location.Location +import com.kouros.navigation.data.BoundingBox +import org.maplibre.geojson.FeatureCollection +import org.maplibre.geojson.Point +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.dsl.addFeature +import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection +import org.maplibre.spatialk.geojson.dsl.buildLineString +import org.maplibre.spatialk.geojson.toJson +import org.maplibre.turf.TurfMeasurement +import org.maplibre.turf.TurfMisc +import java.lang.Math.toDegrees +import java.lang.Math.toRadians +import kotlin.math.cos +import kotlin.math.pow + +object GeoUtils { + + fun snapLocation(location: Location, stepCoordinates: List) : Location { + val newLocation = Location(location) + val oldPoint = Point.fromLngLat(location.longitude, location.latitude) + if (stepCoordinates.size > 1) { + val pointFeature = TurfMisc.nearestPointOnLine(oldPoint, stepCoordinates) + val point = pointFeature.geometry() as Point + newLocation.latitude = point.latitude() + newLocation.longitude = point.longitude() + } + return newLocation + } + + fun decodePolyline(encoded: String, vararg precisionOptional: Int): List> { + val precision = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6 + val factor = 10.0.pow(precision) + + var lat = 0 + var lng = 0 + val coordinates = mutableListOf>() + var index = 0 + + while (index < encoded.length) { + var byte = 0x20 + var shift = 0 + var result = 0 + while (byte >= 0x20) { + byte = encoded[index].code - 63 + result = result or ((byte and 0x1f) shl shift) + shift += 5 + index++ + } + lat += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) + + byte = 0x20 + shift = 0 + result = 0 + while (byte >= 0x20) { + byte = encoded[index].code - 63 + result = result or ((byte and 0x1f) shl shift) + shift += 5 + index++ + } + lng += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) + coordinates.add(listOf(lng.toDouble() / factor, lat.toDouble() / factor)) + } + + return coordinates + } + + /** + * Calculates the geographic center of the route's GeoJSON data. + * + * @return A [Location] object representing the center point. + * @throws IllegalStateException if the calculated center does not have valid Point geometry. + */ + fun createCenterLocation(routeGeoJson: String): Location { + // 1. Create a FeatureCollection from the raw GeoJSON string. + val featureCollection = FeatureCollection.fromJson(routeGeoJson) + + // 2. Calculate the center feature of the collection. + val centerFeature = TurfMeasurement.center(featureCollection) + + // 3. Safely access and cast the geometry, throwing an informative error if it fails. + val centerPoint = centerFeature.geometry() as? Point + ?: throw IllegalStateException("Center of GeoJSON is not a valid Point.") + + // 4. Create and return the Location object. + return location(centerPoint.longitude(), centerPoint.latitude()) + } + + fun createLineStringCollection(lineCoordinates: List>): String { + val lineString = buildLineString { + lineCoordinates.forEach { + add(org.maplibre.spatialk.geojson.Point( + it[0], + it[1] + )) + } + } + val feature = Feature(lineString, null) + val featureCollection = org.maplibre.spatialk.geojson.FeatureCollection(feature) + return featureCollection.toJson() + } + + fun createPointCollection(lineCoordinates: List>): String { + val featureCollection = buildFeatureCollection { + lineCoordinates.forEach { + addFeature { + geometry = org.maplibre.spatialk.geojson.Point(it[0], it[1]) + properties = null + } + } + } + return featureCollection.toJson() + } + + fun getOverpassBbox(location: Location, radius: Double): String { + + val bbox = getBoundingBox(location.longitude, location.latitude, radius) + val neLon = bbox["ne"]?.get("lon") + val neLat = bbox["ne"]?.get("lat") + val swLon = bbox["sw"]?.get("lon") + val swLat = bbox["sw"]?.get("lat") + return "$swLon,$swLat,$neLon,$neLat" + } + + fun getBoundingBox2(location: Location, radius: Double): BoundingBox { + val bbox = getBoundingBox(location.longitude, location.latitude, radius) + val neLon = bbox["ne"]?.get("lon") + val neLat = bbox["ne"]?.get("lat") + val swLon = bbox["sw"]?.get("lon") + val swLat = bbox["sw"]?.get("lat") + return BoundingBox(swLat ?: 0.0 , swLon ?: 0.0, neLat ?: 0.0, neLon ?: 0.0) + } + fun getBoundingBox( + lat: Double, + lon: Double, + radius: Double + ): Map> { + val earthRadius = 6371.0 + val maxLat = lat + toDegrees(radius / earthRadius) + val minLat = lat - toDegrees(radius / earthRadius) + val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat))) + val minLon = lon - toDegrees(radius / earthRadius / cos(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) + ) + } +} \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt index 6f444f8..be869b8 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt @@ -6,12 +6,13 @@ import android.location.LocationManager import androidx.core.content.edit import com.kouros.navigation.data.BoundingBox import com.kouros.navigation.data.Constants.SHARED_PREF_KEY -import com.kouros.navigation.data.GeoJsonFeature -import com.kouros.navigation.data.GeoJsonFeatureCollection -import com.kouros.navigation.data.GeoJsonType -import kotlinx.serialization.json.Json import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.Point +import org.maplibre.spatialk.geojson.Feature +import org.maplibre.spatialk.geojson.dsl.addFeature +import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection +import org.maplibre.spatialk.geojson.dsl.buildLineString +import org.maplibre.spatialk.geojson.toJson import org.maplibre.turf.TurfMeasurement import org.maplibre.turf.TurfMisc import java.lang.Math.toDegrees @@ -77,121 +78,6 @@ object NavigationUtils { apply() } } - fun snapLocation(location: Location, stepCoordinates: List) : Location { - val newLocation = Location(location) - val oldPoint = Point.fromLngLat(location.longitude, location.latitude) - if (stepCoordinates.size > 1) { - val pointFeature = TurfMisc.nearestPointOnLine(oldPoint, stepCoordinates) - val point = pointFeature.geometry() as Point - newLocation.latitude = point.latitude() - newLocation.longitude = point.longitude() - } - return newLocation - } - - fun decodePolyline(encoded: String, vararg precisionOptional: Int): List> { - val precision = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6 - val factor = 10.0.pow(precision) - - var lat = 0 - var lng = 0 - val coordinates = mutableListOf>() - var index = 0 - - while (index < encoded.length) { - var byte = 0x20 - var shift = 0 - var result = 0 - while (byte >= 0x20) { - byte = encoded[index].code - 63 - result = result or ((byte and 0x1f) shl shift) - shift += 5 - index++ - } - lat += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) - - byte = 0x20 - shift = 0 - result = 0 - while (byte >= 0x20) { - byte = encoded[index].code - 63 - result = result or ((byte and 0x1f) shl shift) - shift += 5 - index++ - } - lng += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) - coordinates.add(listOf(lng.toDouble() / factor, lat.toDouble() / factor)) - } - - return coordinates - } - - /** - * Calculates the geographic center of the route's GeoJSON data. - * - * @return A [Location] object representing the center point. - * @throws IllegalStateException if the calculated center does not have valid Point geometry. - */ - fun createCenterLocation(routeGeoJson: String): Location { - // 1. Create a FeatureCollection from the raw GeoJSON string. - val featureCollection = FeatureCollection.fromJson(routeGeoJson) - - // 2. Calculate the center feature of the collection. - val centerFeature = TurfMeasurement.center(featureCollection) - - // 3. Safely access and cast the geometry, throwing an informative error if it fails. - val centerPoint = centerFeature.geometry() as? Point - ?: throw IllegalStateException("Center of GeoJSON is not a valid Point.") - - // 4. Create and return the Location object. - return location(centerPoint.longitude(), centerPoint.latitude()) - } - fun createGeoJson(type : String, lineCoordinates: List>): String { - val lineString = GeoJsonType(type = type, 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 getOverpassBbox(location: Location, radius: Double): String { - val bbox = getBoundingBox(location.longitude, location.latitude, radius) - val neLon = bbox["ne"]?.get("lon") - val neLat = bbox["ne"]?.get("lat") - val swLon = bbox["sw"]?.get("lon") - val swLat = bbox["sw"]?.get("lat") - return "$swLon,$swLat,$neLon,$neLat" - } - - fun getBoundingBox2(location: Location, radius: Double): BoundingBox { - val bbox = getBoundingBox(location.longitude, location.latitude, radius) - val neLon = bbox["ne"]?.get("lon") - val neLat = bbox["ne"]?.get("lat") - val swLon = bbox["sw"]?.get("lon") - val swLat = bbox["sw"]?.get("lat") - return BoundingBox(swLat ?: 0.0 , swLon ?: 0.0, neLat ?: 0.0, neLon ?: 0.0) - } - fun getBoundingBox( - lat: Double, - lon: Double, - radius: Double - ): Map> { - val earthRadius = 6371.0 - val maxLat = lat + toDegrees(radius / earthRadius) - val minLat = lat - toDegrees(radius / earthRadius) - val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat))) - val minLon = lon - toDegrees(radius / earthRadius / cos(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 calculateZoom(speed: Double?): Double { @@ -224,6 +110,17 @@ fun previewZoom(previewDistance: Double): Double { } return 9.0 } + + +fun calcTilt(newZoom: Double, tilt: Double): Double = if (newZoom < 13) { + 0.0 +} else { + if (tilt == 0.0) { + 55.0 + } else { + tilt + } +} fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double) : Double { val distance = fromLocation.distanceTo(toLocation) if (distance < 1.0) {