This commit is contained in:
Dimitris
2025-12-16 07:19:29 +01:00
parent 72872cddeb
commit d546ede0e5
22 changed files with 589 additions and 137 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 8 versionCode = 9
versionName = "0.1.3.8" versionName = "0.1.3.9"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -45,6 +45,7 @@ import androidx.lifecycle.Observer
import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.homeLocation import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
@@ -110,18 +111,19 @@ class MainActivity : ComponentActivity() {
private var loadRecentPlaces = false private var loadRecentPlaces = false
private var overpass = false
init { init {
viewModel.route.observe(this, observer) viewModel.route.observe(this, observer)
} }
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]) @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (useMock) { if (useMock) {
checkMockLocationEnabled() checkMockLocationEnabled()
} }
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
if (useMock) { if (useMock) {
@@ -241,6 +243,10 @@ class MainActivity : ComponentActivity() {
&& lastLocation.latitude != location.position.latitude && lastLocation.latitude != location.position.latitude
&& lastLocation.longitude != location.position.longitude && lastLocation.longitude != location.position.longitude
) { ) {
if (lastLocation.latitude != 0.0 && !overpass) {
//viewModel.getAmenities(Constants.CHARGING_STATION, lastLocation)
//overpass = true
}
val currentLocation = location(location.position.longitude, location.position.latitude) val currentLocation = location(location.position.longitude, location.position.latitude)
with(routeModel) { with(routeModel) {
if (isNavigating()) { if (isNavigating()) {

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -64,27 +65,18 @@ class SurfaceRenderer(
) )
var stableArea = Rect() var stableArea = Rect()
var width = 0 var width = 0
var height = 0 var height = 0
var lastBearing = 0.0 var lastBearing = 0.0
val routeData = MutableLiveData("") val routeData = MutableLiveData("")
val previewRouteData = MutableLiveData("")
val speed = MutableLiveData(0F) val speed = MutableLiveData(0F)
lateinit var centerLocation: Location lateinit var centerLocation: Location
var preview = false var preview = false
var previewDistance = 0.0
val previewRouteData = MutableLiveData("")
lateinit var mapView: ComposeView lateinit var mapView: ComposeView
var panView = false var panView = false
var tilt = 55.0 var tilt = 55.0
var previewDistance = 0.0
var countDownTimerActive = false var countDownTimerActive = false
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback { val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
@@ -341,6 +333,18 @@ class SurfaceRenderer(
previewDistance = routeModel.route.distance previewDistance = routeModel.route.distance
} }
fun setLocation(location: Location) {
cameraPosition.postValue(
cameraPosition.value!!.copy(
bearing = 0.0,
zoom = 15.0,
tilt = 0.0,
padding = PaddingValues(start = 350.dp, bottom = 0.dp),
target = Position(location.longitude, location.latitude)
)
)
}
companion companion
object { object {
private const val TAG = "MapRenderer" private const val TAG = "MapRenderer"

View File

@@ -166,7 +166,7 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int) {
Canvas(modifier =Modifier Canvas(modifier =Modifier
.size(imageSize.dp, imageSize.dp)) { .size(imageSize.dp, imageSize.dp)) {
scale(scaleX = 1f, scaleY = 0.7f) { scale(scaleX = 1f, scaleY = 0.7f) {
drawCircle(Color.DarkGray.copy(alpha = 0.4f)) drawCircle(Color.DarkGray.copy(alpha = 0.2f))
} }
} }
Icon( Icon(

View File

@@ -0,0 +1,69 @@
package com.kouros.navigation.car.screen
import android.location.Location
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants
class CategoriesScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val location: Location,
) : Screen(carContext) {
var categories: List<Category> = listOf(
Category(id = Constants.GAS_STATION, name = carContext.getString(R.string.gas_station)),
Category(id = Constants.PHARMACY, name = carContext.getString(R.string.pharmacy)),
Category(id = Constants.CHARGING_STATION, name = carContext.getString(R.string.charging_station))
)
override fun onGetTemplate(): Template {
val itemListBuilder = ItemList.Builder()
.setNoItemsMessage("No categories to show")
categories.forEach {
it.name
itemListBuilder.addItem(
Row.Builder()
.setTitle(it.name)
.setOnClickListener {
screenManager
.pushForResult(
CategoryScreen(
carContext,
surfaceRenderer,
location,
it.id
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
.setBrowsable(true)
.build()
)
}
val header = Header.Builder()
.setStartHeaderAction(Action.BACK)
.setTitle("title")
.build()
return ListTemplate.Builder()
.setHeader(header)
.setSingleList(itemListBuilder.build())
.build()
}
}

View File

@@ -0,0 +1,143 @@
package com.kouros.navigation.car.screen
import android.location.Location
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location
import kotlin.math.min
class CategoryScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
location: Location,
category: String,
) : Screen(carContext) {
val viewModel = ViewModel(NavigationRepository())
var elements = listOf<Elements>()
val observer = Observer<List<Elements>> { newElements ->
elements = newElements
invalidate()
}
init {
viewModel.elements.observe(this, observer)
viewModel.getAmenities(category, location)
invalidate()
}
override fun onGetTemplate(): Template {
val listBuilder = ItemList.Builder()
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
val listLimit = min(
10,
carContext.getCarService(ConstraintManager::class.java)
.getContentLimit(
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
)
)
elements.forEach {
//listBuilder.addItem(createRow(it.tags!!.operator.toString(), it.tags?.capacity.toString()))
listBuilder.addItem(
Row.Builder()
.setOnClickListener({
val location = location(it.lon!!, it.lat!!)
surfaceRenderer.setLocation(location)
})
.setTitle(secondText(it.tags?.capacity.toString()))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
com.kouros.android.cars.carappservice.R.drawable.ev_station_24px
)
)
.build()
)
.addText(it.tags!!.operator.toString())
.addText(secondText(it.tags?.capacity.toString()))
.build()
)
}
}
val header = Header.Builder()
.setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.charging_station))
.build()
val actionStrip = ActionStrip.Builder()
.addAction(
Action.Builder()
.setOnClickListener {
CarToast.makeText(
carContext,
carContext.getString(
R.string.notification_demo
),
CarToast.LENGTH_SHORT
)
.show()
}
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_place_white_24dp
)
).build()
)
.setFlags(Action.FLAG_IS_PERSISTENT)
.build()
)
.build()
val builder = MapWithContentTemplate.Builder()
.setContentTemplate(
ListTemplate.Builder()
.setHeader(header)
.setSingleList(listBuilder.build())
.build()
)
.setActionStrip(actionStrip)
return builder.build()
}
private fun secondText(sText: String): CarText {
val secondText =
CarText.Builder(
"================= " + sText + " ================"
)
.addVariant(
("--------------------- " + sText
+ " ----------------------")
)
.addVariant(sText)
.build()
return secondText
}
}

View File

@@ -22,6 +22,7 @@ import com.kouros.data.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.categories
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
@@ -152,6 +153,7 @@ class PlaceListScreen(
R.string.recent_Item_deleted, CarToast.LENGTH_LONG R.string.recent_Item_deleted, CarToast.LENGTH_LONG
).show() ).show()
loadPlaces() loadPlaces()
invalidate()
} }
.build() .build()

View File

@@ -26,6 +26,7 @@ import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_PRIMARY
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
@@ -94,6 +95,7 @@ class RoutePreviewScreen(
) )
).build() ).build()
val navigateAction = Action.Builder() val navigateAction = Action.Builder()
.setFlags(FLAG_PRIMARY)
.setIcon(navigateActionIcon) .setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate() } .setOnClickListener { this.onNavigate() }
.build() .build()
@@ -108,58 +110,10 @@ class RoutePreviewScreen(
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.route_preview)) .setTitle(carContext.getString(R.string.route_preview))
.addEndHeaderAction( .addEndHeaderAction(
Action.Builder() favoriteAction()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (mIsFavorite)
R.drawable.ic_favorite_filled_white_24dp
else
R.drawable.ic_favorite_white_24dp
)
)
.build()
)
.setOnClickListener {
mIsFavorite = !mIsFavorite
CarToast.makeText(
carContext,
if (mIsFavorite)
carContext
.getString(R.string.favorites)
else
carContext.getString(
R.string.not_favorite_toast_msg
),
CarToast.LENGTH_SHORT
)
.show()
vieModel.saveFavorite(destination)
println(destination)
invalidate()
}
.build()
) )
.addEndHeaderAction( .addEndHeaderAction(
Action.Builder() deleteFavoriteAction()
.setOnClickListener {
if (mIsFavorite) {
vieModel.deleteFavorite(destination)
}
mIsFavorite = !mIsFavorite
finish()
}
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_delete_foreground
)
)
.build()
)
.build()
) )
.build() .build()
@@ -186,6 +140,56 @@ class RoutePreviewScreen(
.build() .build()
} }
private fun favoriteAction(): Action = Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (mIsFavorite)
R.drawable.ic_favorite_filled_white_24dp
else
R.drawable.ic_favorite_white_24dp
)
)
.build()
)
.setOnClickListener {
mIsFavorite = !mIsFavorite
CarToast.makeText(
carContext,
if (mIsFavorite)
carContext
.getString(R.string.favorites)
else
carContext.getString(
R.string.not_favorite_toast_msg
),
CarToast.LENGTH_SHORT
)
.show()
vieModel.saveFavorite(destination)
invalidate()
}
.build()
private fun deleteFavoriteAction(): Action = Action.Builder()
.setOnClickListener {
if (mIsFavorite) {
vieModel.deleteFavorite(destination)
}
mIsFavorite = !mIsFavorite
finish()
}
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_delete_foreground
)
)
.build()
)
.build()
private fun createRow(index: Int, action: Action): Row { private fun createRow(index: Int, action: Action): Row {
val route: CarText = createRouteText(index) val route: CarText = createRouteText(index)
return Row.Builder() return Row.Builder()

View File

@@ -34,7 +34,8 @@ class SearchScreen(
var categories: List<Category> = listOf( var categories: List<Category> = listOf(
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)), Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)), //Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)),
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites)) Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites))
) )
@@ -66,20 +67,36 @@ class SearchScreen(
.setTitle(it.name) .setTitle(it.name)
.setImage(categoryIcon(it.id)) .setImage(categoryIcon(it.id))
.setOnClickListener { .setOnClickListener {
screenManager if (it.id == Constants.CATEGORIES) {
.pushForResult( screenManager
PlaceListScreen( .pushForResult(
carContext, CategoriesScreen(
surfaceRenderer, carContext,
location, surfaceRenderer,
it.id location,
) )
) { obj: Any? -> ) { obj: Any? ->
if (obj != null) { if (obj != null) {
setResult(obj) setResult(obj)
finish() finish()
}
} }
} } else {
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
location,
it.id
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
} }
.setBrowsable(true) .setBrowsable(true)
.build() .build()

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M340,760L440,600L380,600L380,480L280,640L340,640L340,760ZM240,400L480,400L480,200Q480,200 480,200Q480,200 480,200L240,200Q240,200 240,200Q240,200 240,200L240,400ZM240,760L480,760L480,480L240,480L240,760ZM160,840L160,200Q160,167 183.5,143.5Q207,120 240,120L480,120Q513,120 536.5,143.5Q560,167 560,200L560,480L610,480Q639,480 659.5,500.5Q680,521 680,550L680,735Q680,752 694,766Q708,780 725,780Q743,780 756.5,766Q770,752 770,735L770,360L760,360Q743,360 731.5,348.5Q720,337 720,320L720,240L740,240L740,180L780,180L780,240L820,240L820,180L860,180L860,240L880,240L880,320Q880,337 868.5,348.5Q857,360 840,360L830,360L830,735Q830,777 799.5,808.5Q769,840 725,840Q682,840 651,808.5Q620,777 620,735L620,550Q620,545 617.5,542.5Q615,540 610,540L560,540L560,840L160,840ZM480,760L240,760L240,760L480,760Z"/>
</vector>

View File

@@ -57,8 +57,9 @@ dependencies {
annotationProcessor(libs.objectbox.processor) annotationProcessor(libs.objectbox.processor)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
implementation("hu.supercluster:overpasser:0.2.2")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -134,6 +134,13 @@ data class ValhallaLocation (
var language: String var language: String
) )
data class BoundingBox (
var southernLat : Double,
var westernLon: Double,
var northerLat : Double,
var easternLon : Double
)
object Constants { object Constants {
const val STYLE: String = "https://kouros-online.de/liberty.json" const val STYLE: String = "https://kouros-online.de/liberty.json"
@@ -141,12 +148,22 @@ object Constants {
//const val STYLE: String = "https://tiles.openfreemap.org/styles/liberty" //const val STYLE: String = "https://tiles.openfreemap.org/styles/liberty"
const val TAG: String = "Navigation" const val TAG: String = "Navigation"
const val CATEGORIES: String = "Categories"
const val CONTACTS: String = "Contacts" const val CONTACTS: String = "Contacts"
const val RECENT: String = "Recent" const val RECENT: String = "Recent"
const val FAVORITES: String = "Favorites" const val FAVORITES: String = "Favorites"
const val GAS_STATION: String ="GasStation"
const val PHARMACY: String ="pharmacy"
const val CHARGING_STATION: String ="charging_station"
val categories = listOf("Tankstelle", "Apotheke", "Ladestationen")
/** The initial location to use as an anchor for searches. */ /** The initial location to use as an anchor for searches. */
val homeLocation: Location = Location(LocationManager.GPS_PROVIDER) val homeLocation: Location = Location(LocationManager.GPS_PROVIDER)
val home2Location: Location = Location(LocationManager.GPS_PROVIDER) val home2Location: Location = Location(LocationManager.GPS_PROVIDER)
@@ -176,7 +193,7 @@ object Constants {
const val MAXIMAL_ROUTE_DEVIATION = 100.0 const val MAXIMAL_ROUTE_DEVIATION = 100.0
const val DESTINATION_ARRIVAL_DISTANCE = 20.0 const val DESTINATION_ARRIVAL_DISTANCE = 40.0
} }

View File

@@ -17,8 +17,8 @@
package com.kouros.navigation.data package com.kouros.navigation.data
import android.location.Location import android.location.Location
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBoundingBox
import org.json.JSONArray import org.json.JSONArray
import java.net.Authenticator import java.net.Authenticator
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -98,7 +98,6 @@ class NavigationRepository {
return places return places
} }
private fun fetchUrl(url: String, authenticator : Boolean): String { private fun fetchUrl(url: String, authenticator : Boolean): String {
try { try {
if (authenticator) { if (authenticator) {

View File

@@ -0,0 +1,72 @@
package com.kouros.navigation.data
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.overpass.Amenity
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.utils.NavigationUtils.getBoundingBox2
import com.kouros.navigation.utils.NavigationUtils.getOverpassBbox
import hu.supercluster.overpasser.library.output.OutputFormat
import hu.supercluster.overpasser.library.output.OutputModificator
import hu.supercluster.overpasser.library.output.OutputOrder
import hu.supercluster.overpasser.library.output.OutputVerbosity
import hu.supercluster.overpasser.library.query.OverpassQuery
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
class Overpass {
fun getAmenities(category: String, location: Location) : List<Elements> {
val boundingBox = getOverpassBbox(location, 2.0)
val bb = getBoundingBox2(location, 2.0)
val url = "https://overpass.kumi.systems/api/interpreter"
val httpURLConnection = URL(url).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(
"Accept",
"application/json"
)
httpURLConnection.setDoOutput(true);
val query = OverpassQuery()
.format(OutputFormat.JSON)
.timeout(30)
.filterQuery()
.node()
.amenity("charging_station")
.tagNot("access", "private")
.boundingBox(bb.southernLat, bb.westernLon, bb.northerLat, bb.easternLon)
.end()
.output(OutputVerbosity.BODY, OutputModificator.CENTER, OutputOrder.QT, 100)
.build()
// define a query
val test = """
|[out:json];
|(
| node[amenity=$category]
| ($boundingBox);
|);
|out body;
""".trimMargin()
// Send the JSON we created
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
outputStreamWriter.write(test)
outputStreamWriter.flush()
// Check if the connection is successful
val responseCode = httpURLConnection.responseCode
println("Overpass: $responseCode")
if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader()
.use { it.readText() } // defaults to UTF-8
val gson = GsonBuilder().serializeNulls().create()
val overpass = gson.fromJson(response, Amenity::class.java)
println("Overpass: $response")
return overpass.elements
}
return emptyList()
}
}

View File

@@ -0,0 +1,13 @@
package com.kouros.navigation.data.overpass
import com.google.gson.annotations.SerializedName
data class Amenity (
@SerializedName("version" ) var version : Double? = null,
@SerializedName("generator" ) var generator : String? = null,
@SerializedName("osm3s" ) var osm3s : Osm3s? = Osm3s(),
@SerializedName("elements" ) var elements : ArrayList<Elements> = arrayListOf()
)

View File

@@ -0,0 +1,14 @@
package com.kouros.navigation.data.overpass
import com.google.gson.annotations.SerializedName
data class Elements (
@SerializedName("type" ) var type : String? = null,
@SerializedName("id" ) var id : Long? = null,
@SerializedName("lat" ) var lat : Double? = null,
@SerializedName("lon" ) var lon : Double? = null,
@SerializedName("tags" ) var tags : Tags? = Tags()
)

View File

@@ -0,0 +1,11 @@
package com.kouros.navigation.data.overpass
import com.google.gson.annotations.SerializedName
data class Osm3s (
@SerializedName("timestamp_osm_base" ) var timestampOsmBase : String? = null,
@SerializedName("copyright" ) var copyright : String? = null
)

View File

@@ -0,0 +1,22 @@
package com.kouros.navigation.data.overpass
import com.google.gson.annotations.SerializedName
data class Tags (
@SerializedName("amenity" ) var amenity : String? = null,
@SerializedName("authentication:none" ) var authenticationNone : String? = null,
@SerializedName("capacity" ) var capacity : String? = null,
@SerializedName("motorcar" ) var motorcar : String? = null,
@SerializedName("network" ) var network : String? = null,
@SerializedName("opening_hours" ) var openingHours : String? = null,
@SerializedName("operator" ) var operator : String? = null,
@SerializedName("operator:short" ) var operatorShort : String? = null,
@SerializedName("operator:wikidata" ) var operatorWikidata : String? = null,
@SerializedName("operator:wikipedia" ) var operatorWikipedia : String? = null,
@SerializedName("ref" ) var ref : String? = null,
@SerializedName("socket:type2" ) var socketType2 : String? = null,
@SerializedName("socket:type2:output" ) var socketType2Ooutput : String? = null
)

View File

@@ -22,7 +22,7 @@
"id": "background", "id": "background",
"type": "background", "type": "background",
"layout": {"visibility": "visible"}, "layout": {"visibility": "visible"},
"paint": {"background-color": "rgba(28, 28, 35, 1)"} "paint": {"background-color": "rgba(39, 46, 57, 1)"}
}, },
{ {
"id": "natural_earth", "id": "natural_earth",
@@ -903,7 +903,11 @@
["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true], ["match", ["get", "brunnel"], ["bridge", "tunnel"], false, true],
["match", ["get", "class"], ["service", "track"], true, false] ["match", ["get", "class"], ["service", "track"], true, false]
], ],
"layout": {"line-cap": "round", "line-join": "round"}, "layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"paint": { "paint": {
"line-width": [ "line-width": [
"interpolate", "interpolate",
@@ -916,7 +920,7 @@
20, 20,
11 11
], ],
"line-color": "rgba(65, 74, 92, 1)" "line-color": "rgba(77, 94, 123, 1)"
} }
}, },
{ {
@@ -1005,7 +1009,7 @@
], ],
"layout": {"line-cap": "round", "line-join": "round"}, "layout": {"line-cap": "round", "line-join": "round"},
"paint": { "paint": {
"line-color": "#e9ac77", "line-color": "rgba(65, 74, 92, 1)",
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],
@@ -1029,7 +1033,7 @@
], ],
"layout": {"line-join": "round"}, "layout": {"line-join": "round"},
"paint": { "paint": {
"line-color": "#e9ac77", "line-color": "rgba(65, 74, 92, 1)",
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],
@@ -1059,7 +1063,7 @@
], ],
"layout": {"line-cap": "round", "line-join": "round"}, "layout": {"line-cap": "round", "line-join": "round"},
"paint": { "paint": {
"line-color": "#e9ac77", "line-color": "rgba(65, 74, 92, 1)",
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],
@@ -1245,7 +1249,7 @@
], ],
"layout": {"line-cap": "round", "line-join": "round"}, "layout": {"line-cap": "round", "line-join": "round"},
"paint": { "paint": {
"line-color": "rgba(105, 170, 87, 1)", "line-color": "rgba(65, 74, 92, 1)",
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],
@@ -1271,7 +1275,7 @@
], ],
"layout": {"line-join": "round"}, "layout": {"line-join": "round"},
"paint": { "paint": {
"line-color": "#fea", "line-color": "rgba(65, 74, 92, 1)",
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],
@@ -1306,7 +1310,7 @@
5, 5,
"hsl(26,87%,62%)", "hsl(26,87%,62%)",
6, 6,
"#fc8" "#ab9"
], ],
"line-width": [ "line-width": [
"interpolate", "interpolate",
@@ -1847,7 +1851,7 @@
], ],
"layout": {"line-join": "round"}, "layout": {"line-join": "round"},
"paint": { "paint": {
"line-color": "#fea", "line-color": "rgba(65, 74, 92, 34)",
"line-width": [ "line-width": [
"interpolate", "interpolate",
["exponential", 1.2], ["exponential", 1.2],

View File

@@ -4,6 +4,8 @@ import android.location.Location
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.ManeuverType import com.kouros.navigation.data.ManeuverType
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
@@ -35,6 +37,7 @@ open class RouteModel() {
set(value) { set(value) {
routeState = routeState.copy(route = value) routeState = routeState.copy(route = value)
} }
fun startNavigation(routeString: String) { fun startNavigation(routeString: String) {
val newRoute = Route.Builder() val newRoute = Route.Builder()
.route(routeString) .route(routeString)
@@ -49,7 +52,7 @@ open class RouteModel() {
this.routeState = routeState.copy( this.routeState = routeState.copy(
route = null, route = null,
isNavigating = false, isNavigating = false,
// destination = Place(), // destination = Place(),
arrived = false, arrived = false,
maneuverType = 0, maneuverType = 0,
currentShapeIndex = 0, currentShapeIndex = 0,
@@ -100,30 +103,31 @@ open class RouteModel() {
} }
fun currentStep(): StepData { fun currentStep(): StepData {
val currentManeuver = route.currentManeuver()
// Determine if we should display the current or the next maneuver // Determine if we should display the current or the next maneuver
val distanceToNextStep = leftStepDistance() val distanceToNextStep = leftStepDistance()
val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD
val shouldAdvance = val shouldAdvance =
isNearNextManeuver && route.currentManeuverIndex < (route.maneuvers.size - 1) isNearNextManeuver && route.currentManeuverIndex < (route.maneuvers.size)
// Determine the maneuver type and corresponding icon
var maneuverType = if (hasArrived(currentManeuver.type)) {
currentManeuver.type
} else {
ManeuverType.None.value
}
// Get the single, correct maneuver for this state // Get the single, correct maneuver for this state
val relevantManeuver = if (shouldAdvance) { val relevantManeuver = if (shouldAdvance) {
route.nextManeuver() // This advances the route's state route.nextManeuver() // This advances the route's state
} else { } else {
route.currentManeuver() route.currentManeuver()
} }
// Safely get the street name from the maneuver // Safely get the street name from the maneuver
val streetName = relevantManeuver.streetNames?.firstOrNull() ?: "" val streetName = relevantManeuver.streetNames?.firstOrNull() ?: ""
if (shouldAdvance) {
// Determine the maneuver type and corresponding icon maneuverType = relevantManeuver.type
val maneuverType = if (hasArrived(relevantManeuver.type)) {
relevantManeuver.type
} else {
ManeuverType.None.value
} }
val maneuverIconPair = maneuverIcon(maneuverType) val maneuverIconPair = maneuverIcon(maneuverType)
// Construct and return the final StepData object // Construct and return the final StepData object
return StepData( return StepData(
streetName, streetName,
@@ -135,6 +139,40 @@ open class RouteModel() {
) )
} }
fun currentStepOld(): StepData {
val maneuver = route.currentManeuver()
var text = ""
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
}
val distanceStepLeft = leftStepDistance()
when (distanceStepLeft) {
in 0.0..Constants.NEXT_STEP_THRESHOLD -> {
if (route.currentManeuverIndex < route.maneuvers.size) {
val maneuver = route.nextManeuver()
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
}
}
}
}
val type = if (hasArrived(maneuver.type)) {
maneuver.type
} else {
ManeuverType.None.value
}
var routing: (Pair<Int, Int>) = maneuverIcon(type)
when (distanceStepLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
if (route.currentManeuverIndex < route.maneuvers.size) {
val maneuver = route.nextManeuver()
val maneuverType = maneuver.type
routing = maneuverIcon(maneuverType)
}
}
}
return StepData(text, distanceStepLeft, routing.first, routing.second, arrivalTime(), travelLeftDistance())
}
fun nextStep(): StepData { fun nextStep(): StepData {
val maneuver = route.nextManeuver() val maneuver = route.nextManeuver()
val maneuverType = maneuver.type val maneuverType = maneuver.type
@@ -290,12 +328,10 @@ open class RouteModel() {
return routeState.isNavigating return routeState.isNavigating
} }
fun isArrived(): Boolean {
return routeState.arrived
}
fun hasArrived(type: Int): Boolean { fun hasArrived(type: Int): Boolean {
return type == ManeuverType.DestinationRight.value // return leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE && type == ManeuverType.DestinationRight.value
return type == ManeuverType.DestinationRight.value
|| routeState.maneuverType == ManeuverType.Destination.value || routeState.maneuverType == ManeuverType.Destination.value
|| routeState.maneuverType == ManeuverType.DestinationLeft.value || routeState.maneuverType == ManeuverType.DestinationLeft.value
} }

View File

@@ -13,11 +13,13 @@ import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Locations import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox.boxStore import com.kouros.navigation.data.ObjectBox.boxStore
import com.kouros.navigation.data.Overpass
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.data.SearchFilter import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.nominatim.Search import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.utils.NavigationUtils import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import io.objectbox.kotlin.boxFor import io.objectbox.kotlin.boxFor
@@ -55,6 +57,9 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData<List<Place>>() MutableLiveData<List<Place>>()
} }
val elements: MutableLiveData<List<Elements>> by lazy {
MutableLiveData<List<Elements>>()
}
fun loadRecentPlace(location: Location) { fun loadRecentPlace(location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -113,6 +118,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
.orderDesc(Place_.lastDate) .orderDesc(Place_.lastDate)
.build() .build()
val results = query.find() val results = query.find()
println("Favorites $results")
query.close() query.close()
for (place in results) { for (place in results) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
@@ -231,6 +237,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
return place.address.road return place.address.road
} }
fun getAmenities(category: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities(category, location)
elements.postValue(amenities)
}
}
fun saveFavorite(place: Place) { fun saveFavorite(place: Place) {
place.category = Constants.FAVORITES place.category = Constants.FAVORITES
savePlace(place) savePlace(place)
@@ -258,7 +271,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val current = LocalDateTime.now(ZoneOffset.UTC) val current = LocalDateTime.now(ZoneOffset.UTC)
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond() place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
placeBox.put(place) placeBox.put(place)
println("Save Recent $place") println("Save Place $place")
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationManager 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.Constants.SHARED_PREF_KEY import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
import com.kouros.navigation.data.GeoJsonFeature import com.kouros.navigation.data.GeoJsonFeature
import com.kouros.navigation.data.GeoJsonFeatureCollection import com.kouros.navigation.data.GeoJsonFeatureCollection
@@ -158,6 +159,23 @@ object NavigationUtils {
return jsonString 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( fun getBoundingBox(
lat: Double, lat: Double,
lon: Double, lon: Double,
@@ -176,30 +194,6 @@ object NavigationUtils {
"se" to mapOf("lat" to minLat, "lon" to maxLon) "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 { fun calculateZoom(speed: Double?): Double {
@@ -208,11 +202,12 @@ fun calculateZoom(speed: Double?): Double {
} }
val speedKmh = (speed * 3.6).toInt() val speedKmh = (speed * 3.6).toInt()
val zoom = when (speedKmh) { val zoom = when (speedKmh) {
in 0..10 -> 17.0 in 0..10 -> 18.0
in 11..30 -> 16.0 in 11..30 -> 17.0
in 31..50 -> 16.0 in 21..40 -> 16.0
in 31..50 -> 15.0
in 51..60 -> 15.0 in 51..60 -> 15.0
else -> 15 else -> 14
} }
return zoom.toDouble() return zoom.toDouble()
} }