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.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.ObjectBox 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 { 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.data.ObjectBox
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calcTilt
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.duration import com.kouros.navigation.utils.duration
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -188,36 +189,17 @@ class SurfaceRenderer(
) { ) {
val cameraDuration = val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing) duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
var bearing = position.bearing val currentSpeed: Float? by speed.observeAsState()
var zoom = position.zoom if (viewStyle == ViewStyle.VIEW) {
var target = position.target DrawImage(paddingValues, currentSpeed, width, height)
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
}
} }
LaunchedEffect(position, viewStyle) { LaunchedEffect(position, viewStyle) {
cameraState.animateTo( cameraState.animateTo(
finalPosition = CameraPosition( finalPosition = CameraPosition(
bearing = bearing, bearing = position.bearing,
zoom = zoom, zoom = position.zoom,
target = target, target = position.target,
tilt = localTilt, tilt = tilt,
padding = paddingValues padding = paddingValues
), ),
duration = cameraDuration duration = cameraDuration
@@ -235,21 +217,15 @@ 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) {
viewStyle = ViewStyle.PAN_VIEW if (viewStyle == ViewStyle.VIEW) {
viewStyle = ViewStyle.PAN_VIEW
}
val newZoom = if (zoomSign < 0) { val newZoom = if (zoomSign < 0) {
cameraPosition.value!!.zoom - 1.0 cameraPosition.value!!.zoom - 1.0
} else { } else {
cameraPosition.value!!.zoom + 1.0 cameraPosition.value!!.zoom + 1.0
} }
tilt = if (newZoom < 13) { tilt = calcTilt(newZoom, tilt)
0.0
} else {
if (tilt == 0.0) {
55.0
} else {
tilt
}
}
updateCameraPosition( updateCameraPosition(
cameraPosition.value!!.bearing, cameraPosition.value!!.bearing,
newZoom, newZoom,

View File

@@ -112,7 +112,6 @@ fun MapLibre(
@Composable @Composable
fun RouteLayer(routeData: String?, viewStyle: ViewStyle) { fun RouteLayer(routeData: String?, viewStyle: ViewStyle) {
if (routeData != null && routeData.isNotEmpty()) { if (routeData != null && routeData.isNotEmpty()) {
println(routeData)
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
if (viewStyle == ViewStyle.VIEW) { if (viewStyle == ViewStyle.VIEW) {
LineLayer( LineLayer(
@@ -149,10 +148,10 @@ fun RouteLayer(routeData: String?, viewStyle: ViewStyle) {
source = routes, source = routes,
// Convert a drawable resource to a MapLibre image // Convert a drawable resource to a MapLibre image
// drawAsSdf = true allows us to tint the image programmatically // 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! // Now we can apply any color we want!
iconColor = const(Color.Blue), iconColor = const(Color.Red),
iconSize = const(1.5f) iconSize = const(5.0f)
) )
} }
} }
@@ -283,7 +282,7 @@ fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) { return when (viewStyle) {
ViewStyle.VIEW -> PaddingValues(start = 50.dp, top = distanceFromTop(height).dp) ViewStyle.VIEW -> PaddingValues(start = 50.dp, top = distanceFromTop(height).dp)
ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.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() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle("title") .setTitle(carContext.getString(R.string.category_title))
.build() .build()
return ListTemplate.Builder() return ListTemplate.Builder()

View File

@@ -23,11 +23,10 @@ import androidx.lifecycle.Observer
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage 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.NavigationRepository
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel 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 com.kouros.navigation.utils.location
import kotlin.math.min import kotlin.math.min
@@ -50,11 +49,10 @@ class CategoryScreen(
loc.longitude = it.lon!! loc.longitude = it.lon!!
loc.latitude = it.lat!! loc.latitude = it.lat!!
} }
if (coordinates.isEmpty()) coordinates.add(listOf(it.lon!!, it.lat!!))
coordinates.add(listOf(location.longitude, location.latitude))
} }
if (elements.isNotEmpty()) { if (elements.isNotEmpty()) {
val route = createGeoJson("Point", coordinates) val route = createPointCollection(coordinates)
surfaceRenderer.setCategories(loc, route) surfaceRenderer.setCategories(loc, route)
invalidate() invalidate()
} }
@@ -106,7 +104,6 @@ class CategoryScreen(
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.charging_station)) .setTitle(carContext.getString(R.string.charging_station))
.build() .build()
val builder = MapWithContentTemplate.Builder() val builder = MapWithContentTemplate.Builder()
.setContentTemplate( .setContentTemplate(
ListTemplate.Builder() 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 @Serializable
data class Locations ( data class Locations (
var lat : Double, 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.Summary
import com.kouros.navigation.data.valhalla.Trip import com.kouros.navigation.data.valhalla.Trip
import com.kouros.navigation.data.valhalla.ValhallaJson import com.kouros.navigation.data.valhalla.ValhallaJson
import com.kouros.navigation.utils.NavigationUtils.createCenterLocation import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.NavigationUtils.createGeoJson import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.NavigationUtils.decodePolyline import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point import org.maplibre.geojson.Point
import org.maplibre.turf.TurfMeasurement
import kotlin.math.roundToInt
data class Route( data class Route(
@@ -95,7 +92,7 @@ data class Route(
points.add(point) points.add(point)
} }
pointLocations = points pointLocations = points
routeGeoJson = createGeoJson("LineString", waypoints) routeGeoJson = createLineStringCollection( waypoints)
centerLocation = createCenterLocation(routeGeoJson) centerLocation = createCenterLocation(routeGeoJson)
return Route( return Route(
maneuvers, maneuvers,

View File

@@ -2,6 +2,8 @@ package com.kouros.navigation.data.overpass
import android.location.Location import android.location.Location
import com.google.gson.GsonBuilder 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 com.kouros.navigation.utils.NavigationUtils
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -11,8 +13,8 @@ class Overpass {
val overpassUrl = "https://overpass.kumi.systems/api/interpreter" val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
fun getAmenities(category: String, location: Location) : List<Elements> { fun getAmenities(category: String, location: Location) : List<Elements> {
val boundingBox = NavigationUtils.getOverpassBbox(location, 2.0) val boundingBox = getOverpassBbox(location, 2.0)
val bb = NavigationUtils.getBoundingBox2(location, 2.0) val bb = getBoundingBox2(location, 2.0)
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST" httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty( 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 androidx.core.content.edit
import com.kouros.navigation.data.BoundingBox import com.kouros.navigation.data.BoundingBox
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY 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.FeatureCollection
import org.maplibre.geojson.Point 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.TurfMeasurement
import org.maplibre.turf.TurfMisc import org.maplibre.turf.TurfMisc
import java.lang.Math.toDegrees import java.lang.Math.toDegrees
@@ -77,121 +78,6 @@ object NavigationUtils {
apply() 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 { fun calculateZoom(speed: Double?): Double {
@@ -224,6 +110,17 @@ fun previewZoom(previewDistance: Double): Double {
} }
return 9.0 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 { fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double) : Double {
val distance = fromLocation.distanceTo(toLocation) val distance = fromLocation.distanceTo(toLocation)
if (distance < 1.0) { if (distance < 1.0) {