Amenities GeoUtils

This commit is contained in:
Dimitris
2025-12-17 13:07:29 +01:00
parent ed24e71473
commit b9030dbc50
10 changed files with 198 additions and 197 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -67,26 +67,6 @@ data class StepData (
)
// GeoJSON data classes
@Serializable
data class GeoJsonType(
val type: String,
val coordinates: List<List<Double>>
)
@Serializable
data class GeoJsonFeature(
val type: String,
val geometry: GeoJsonType,
)
@Serializable
data class GeoJsonFeatureCollection(
val type: String,
val features: List<GeoJsonFeature>
)
@Serializable
data class Locations (
var lat : Double,

View File

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

View File

@@ -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<Elements> {
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(

View File

@@ -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<Point>) : 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<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
}
/**
* 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<List<Double>>): 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<List<Double>>): 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<String, Map<String, Double>> {
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)
)
}
}

View File

@@ -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<Point>) : 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<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
}
/**
* 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<List<Double>>): 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<String, Map<String, Double>> {
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) {