Speed radar

This commit is contained in:
Dimitris
2025-12-19 15:03:32 +01:00
parent b9030dbc50
commit 9e453dc955
31 changed files with 597 additions and 412 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 = 10 versionCode = 11
versionName = "0.1.3.10" versionName = "0.1.3.11"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>-->
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="MockLocation" /> tools:ignore="MockLocation" />
@@ -18,6 +19,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.Navigation"> android:theme="@style/Theme.Navigation">
<meta-data <meta-data

View File

@@ -46,6 +46,7 @@ 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
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
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
@@ -87,8 +88,8 @@ class MainActivity : ComponentActivity() {
if (newRoute.isNotEmpty()) { if (newRoute.isNotEmpty()) {
routeModel.startNavigation(newRoute) routeModel.startNavigation(newRoute)
routeData.value = routeModel.route.routeGeoJson routeData.value = routeModel.route.routeGeoJson
//mock.setMockLocation(homeLocation.latitude, homeLocation.longitude)
simulate() simulate()
//test()
} }
} }
val cameraPosition = MutableLiveData( val cameraPosition = MutableLiveData(
@@ -237,10 +238,6 @@ 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()) {
@@ -249,6 +246,13 @@ class MainActivity : ComponentActivity() {
if (route.currentManeuverIndex + 1 <= route.maneuvers.size) { if (route.currentManeuverIndex + 1 <= route.maneuvers.size) {
nextStepData.value = nextStep() nextStepData.value = nextStep()
} }
if (routeState.maneuverType == 39
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
routeState = routeState.copy(arrived = true)
routeData.value = ""
}
} }
} }
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing) val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
@@ -312,4 +316,17 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
fun test() {
for ((index, loc) in routeModel.route.waypoints.withIndex()) {
if (index > 300) {
routeModel.updateLocation(location(loc[0], loc[1]))
routeModel.currentStep()
if (routeModel.route.currentManeuverIndex + 1 <= routeModel.route.maneuvers.size) {
nextStepData.value = routeModel.nextStep()
}
println(routeModel.routeState.maneuverType)
}
}
}
} }

View File

@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
@@ -32,8 +34,10 @@ android {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlin {
jvmTarget = "11" compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
} }
} }

View File

@@ -180,6 +180,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
routeModel.stopNavigation() routeModel.stopNavigation()
} }
companion object { companion object {
var uriHost: String = "navigation" var uriHost: String = "navigation"

View File

@@ -37,7 +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.calculateTilt
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
@@ -225,7 +225,7 @@ class SurfaceRenderer(
} else { } else {
cameraPosition.value!!.zoom + 1.0 cameraPosition.value!!.zoom + 1.0
} }
tilt = calcTilt(newZoom, tilt) tilt = calculateTilt(newZoom, tilt)
updateCameraPosition( updateCameraPosition(
cameraPosition.value!!.bearing, cameraPosition.value!!.bearing,
newZoom, newZoom,
@@ -310,7 +310,7 @@ class SurfaceRenderer(
} }
fun setCategories(location: Location, route: String) { fun setCategories(location: Location, route: String) {
viewStyle = ViewStyle.SEARCH_VIEW viewStyle = ViewStyle.AMENITY_VIEW
routeData.value = route routeData.value = route
updateCameraPosition( updateCameraPosition(
0.0, 0.0,
@@ -319,14 +319,13 @@ class SurfaceRenderer(
) )
} }
fun setCategoryLocation(location: Location) { fun setCategoryLocation(location: Location, category: String) {
viewStyle = ViewStyle.SEARCH_VIEW viewStyle = ViewStyle.AMENITY_VIEW
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
target = Position(location.longitude, location.latitude) target = Position(location.longitude, location.latitude)
) )
) )
} }
companion companion
@@ -337,6 +336,6 @@ class SurfaceRenderer(
enum class ViewStyle { enum class ViewStyle {
VIEW, PREVIEW, PAN_VIEW, SEARCH_VIEW VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW
} }

View File

@@ -33,16 +33,18 @@ import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.expressions.dsl.contains
import org.maplibre.compose.expressions.dsl.exponential import org.maplibre.compose.expressions.dsl.exponential
import org.maplibre.compose.expressions.dsl.image import org.maplibre.compose.expressions.dsl.image
import org.maplibre.compose.expressions.dsl.interpolate import org.maplibre.compose.expressions.dsl.interpolate
import org.maplibre.compose.expressions.dsl.zoom import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.layers.Anchor import org.maplibre.compose.layers.Anchor
import org.maplibre.compose.layers.CircleLayer
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
import org.maplibre.compose.layers.SymbolLayer import org.maplibre.compose.layers.SymbolLayer
@@ -58,9 +60,15 @@ import org.maplibre.compose.sources.Source
import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.geojson.FeatureCollection
import org.maplibre.spatialk.geojson.BoundingBox.Companion.serializer
import org.maplibre.spatialk.geojson.Feature import org.maplibre.spatialk.geojson.Feature
import org.maplibre.spatialk.geojson.FeatureCollection import org.maplibre.spatialk.geojson.GeoJson
import org.maplibre.spatialk.geojson.Geometry
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import org.maplibre.spatialk.geojson.dsl.FeatureBuilder
import org.maplibre.spatialk.geojson.dsl.FeatureCollectionBuilder
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
@Composable @Composable
@@ -103,19 +111,22 @@ fun MapLibre(
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) { if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
BuildingLayer(tiles) BuildingLayer(tiles)
} }
RouteLayer(route, viewStyle) if (viewStyle == ViewStyle.AMENITY_VIEW) {
AmenityLayer(route)
} else {
RouteLayer(route)
}
} }
//Puck(cameraState, lastLocation) //Puck(cameraState, lastLocation)
} }
} }
@Composable @Composable
fun RouteLayer(routeData: String?, viewStyle: ViewStyle) { fun RouteLayer(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) { if (routeData != null && routeData.isNotEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
if (viewStyle == ViewStyle.VIEW) {
LineLayer( LineLayer(
id = "routes-casing$viewStyle", id = "routes-casing",
source = routes, source = routes,
color = const(Color.White), color = const(Color.White),
width = width =
@@ -129,7 +140,7 @@ fun RouteLayer(routeData: String?, viewStyle: ViewStyle) {
), ),
) )
LineLayer( LineLayer(
id = "routes$viewStyle", id = "routes",
source = routes, source = routes,
color = const(RouteColor), color = const(RouteColor),
width = width =
@@ -142,21 +153,27 @@ fun RouteLayer(routeData: String?, viewStyle: ViewStyle) {
20 to const(22.dp), 20 to const(22.dp),
), ),
) )
} else {
SymbolLayer(
id = "my-symbol-layer",
source = routes,
// Convert a drawable resource to a MapLibre image
// drawAsSdf = true allows us to tint the image programmatically
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.Red),
iconSize = const(5.0f)
)
}
} }
} }
@Composable
fun AmenityLayer(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) {
val color = if (routeData.contains(Constants.PHARMACY)) {
const(Color.Red)
} else {
const(Color.Green)
}
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
SymbolLayer(
id = "amenity-layer",
source = routes,
iconImage = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true),
iconColor = color,
iconSize = const(3.0f),
)
}
}
@Composable @Composable
fun BuildingLayer(tiles: Source) { fun BuildingLayer(tiles: Source) {
Anchor.Replace("building-3d") { Anchor.Replace("building-3d") {

View File

@@ -18,7 +18,12 @@ package com.kouros.navigation.car.navigation
import android.text.SpannableString import android.text.SpannableString
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.Alert
import androidx.car.app.model.AlertCallback
import androidx.car.app.model.CarColor import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
@@ -28,8 +33,7 @@ import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate import androidx.car.app.navigation.model.TravelEstimate
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.data.R
import com.kouros.navigation.data.ManeuverType
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -117,4 +121,43 @@ class RouteCarModel() : RouteModel() {
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon { fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build() return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
} }
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String?) {
carContext.getCarService<AppManager?>(AppManager::class.java)
.showAlert(createAlert(carContext, distance, maxSpeed))
}
fun createAlert(carContext: CarContext, distance: Double, maxSpeed: String?): Alert {
val title = createCarText(carContext,R.string.speed_camera)
val subtitle = CarText.create(maxSpeed!!)
val icon = CarIcon.ALERT
val dismissAction: Action = createToastAction(
carContext,
R.string.speed_camera, R.string.exit_action_title,
FLAG_DEFAULT
)
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */10000)
.setSubtitle(subtitle)
.setIcon(icon)
.addAction(dismissAction).setCallback(object : AlertCallback {
override fun onCancel(reason: Int) {
}
override fun onDismiss() {
}
}).build()
}
private fun createToastAction(
carContext: CarContext,
@StringRes titleRes: Int, @StringRes toastStringRes: Int,
flags: Int
): Action {
return Action.Builder()
.setOnClickListener { }
.setTitle(createCarText(carContext,titleRes))
.setFlags(flags)
.build()
}
} }

View File

@@ -15,7 +15,9 @@ import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
class CategoriesScreen( class CategoriesScreen(
private val carContext: CarContext, private val carContext: CarContext,
@@ -24,9 +26,9 @@ class CategoriesScreen(
) : Screen(carContext) { ) : Screen(carContext) {
var categories: List<Category> = listOf( var categories: List<Category> = listOf(
Category(id = Constants.FUEL_STATION, name = carContext.getString(R.string.fuel_station)), Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
Category(id = Constants.PHARMACY, name = carContext.getString(R.string.pharmacy)), Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)),
Category(id = Constants.CHARGING_STATION, name = carContext.getString(R.string.charging_station)) Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station))
) )
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -36,13 +38,7 @@ class CategoriesScreen(
itemListBuilder.addItem( itemListBuilder.addItem(
Row.Builder() Row.Builder()
.setTitle(it.name) .setTitle(it.name)
.setImage(CarIcon.Builder( .setImage(carIcon(carContext,it.id))
IconCompat.createWithResource(
carContext,
com.kouros.android.cars.carappservice.R.drawable.ev_station_24px
)
)
.build())
.setOnClickListener { .setOnClickListener {
screenManager screenManager
.pushForResult( .pushForResult(
@@ -64,7 +60,7 @@ class CategoriesScreen(
) )
} }
surfaceRenderer.viewStyle = ViewStyle.SEARCH_VIEW surfaceRenderer.viewStyle = ViewStyle.AMENITY_VIEW
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
@@ -77,3 +73,19 @@ class CategoriesScreen(
.build() .build()
} }
} }
fun carIcon(context: CarContext, id: String): CarIcon {
val resId = when (id) {
FUEL_STATION -> R.drawable.local_gas_station_48px
PHARMACY -> R.drawable.local_pharmacy_48px
CHARGING_STATION -> R.drawable.ev_station_48px
else -> {}
}
return CarIcon.Builder(
IconCompat.createWithResource(
context,
resId as Int
)
)
.build()
}

View File

@@ -3,12 +3,10 @@ package com.kouros.navigation.car.screen
import android.location.Location import android.location.Location
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.car.app.CarContext import androidx.car.app.CarContext
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.ActionStrip import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
import androidx.car.app.model.Header import androidx.car.app.model.Header
import androidx.car.app.model.ItemList import androidx.car.app.model.ItemList
@@ -17,24 +15,24 @@ import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer 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
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.GeoUtils.createPointCollection import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.round
import kotlin.math.min import kotlin.math.min
class CategoryScreen( class CategoryScreen(
private val carContext: CarContext, private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer, private val surfaceRenderer: SurfaceRenderer,
location: Location, location: Location,
category: String, private val category: String,
) : Screen(carContext) { ) : Screen(carContext) {
val viewModel = ViewModel(NavigationRepository()) val viewModel = ViewModel(NavigationRepository())
@@ -52,7 +50,7 @@ class CategoryScreen(
coordinates.add(listOf(it.lon!!, it.lat!!)) coordinates.add(listOf(it.lon!!, it.lat!!))
} }
if (elements.isNotEmpty()) { if (elements.isNotEmpty()) {
val route = createPointCollection(coordinates) val route = createPointCollection(coordinates, category)
surfaceRenderer.setCategories(loc, route) surfaceRenderer.setCategories(loc, route)
invalidate() invalidate()
} }
@@ -65,12 +63,10 @@ class CategoryScreen(
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val listBuilder = ItemList.Builder() val listBuilder = ItemList.Builder()
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
var index = 0 var index = 0
val listLimit = min( val listLimit = min(
100, 50,
carContext.getCarService(ConstraintManager::class.java) carContext.getCarService(ConstraintManager::class.java)
.getContentLimit( .getContentLimit(
ConstraintManager.CONTENT_LIMIT_TYPE_LIST ConstraintManager.CONTENT_LIMIT_TYPE_LIST
@@ -78,28 +74,14 @@ class CategoryScreen(
) )
elements.forEach { elements.forEach {
if (index++ < listLimit) { if (index++ < listLimit) {
if (it.tags.operator != null) {
listBuilder.addItem( listBuilder.addItem(
Row.Builder() createItem(it, category)
.setOnClickListener {
val location = location(it.lon!!, it.lat!!)
surfaceRenderer.setCategoryLocation(location)
}
.setTitle(it.tags.operator.toString())
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
com.kouros.android.cars.carappservice.R.drawable.ev_station_24px
)
)
.build()
)
.addText(it.tags.network.toString())
.build()
) )
} }
} }
} }
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.charging_station)) .setTitle(carContext.getString(R.string.charging_station))
@@ -116,11 +98,39 @@ class CategoryScreen(
getMapActionStrip() getMapActionStrip()
).build() ).build()
) )
return builder.build() return builder.build()
} }
private fun secondText(sText: String): CarText { private fun createItem(it: Elements, category: String): Row {
var name = ""
if (it.tags.name != null) {
name = it.tags.name.toString()
}
if (name.isEmpty()) {
name = it.tags.operator.toString()
}
val row = Row.Builder()
.setOnClickListener {
val location = location(it.lon!!, it.lat!!)
surfaceRenderer.setCategoryLocation(location, category)
println(it)
}
.setTitle(name)
.setImage(carIcon(carContext, category))
if (it.distance < 1000) {
row.addText("${(it.distance).toInt()} m")
} else {
row.addText("${(it.distance / 1000).round(1)} km")
}
if (category == Constants.CHARGING_STATION) {
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
} else {
row.addText(carText("${it.tags.openingHours}"))
}
return row.build()
}
private fun carText(sText: String): CarText {
val secondText = val secondText =
CarText.Builder( CarText.Builder(
"================= " + sText + " ================" "================= " + sText + " ================"

View File

@@ -1,7 +1,5 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.content.ComponentName
import android.content.Intent
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.CountDownTimer import android.os.CountDownTimer
@@ -9,6 +7,7 @@ import android.os.Handler
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
@@ -21,18 +20,18 @@ import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo import androidx.car.app.navigation.model.RoutingInfo
import androidx.car.app.notification.CarPendingIntent
import androidx.car.app.suggestion.model.Suggestion
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.NavigationCarAppService
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.ViewStyle
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.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
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.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -41,7 +40,6 @@ class NavigationScreen(
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var routeModel: RouteCarModel, private var routeModel: RouteCarModel,
private var listener: Listener private var listener: Listener
) : ) :
Screen(carContext) { Screen(carContext) {
@@ -52,20 +50,14 @@ class NavigationScreen(
} }
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER) var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlace = Place()
lateinit var recentPlace: Place var navigationType = NavigationType.VIEW
var recentPlaceFound = false
var recentPlaceActive = true
var calculateNewRoute = false
val viewModel = ViewModel(NavigationRepository()) val viewModel = ViewModel(NavigationRepository())
val observer = Observer<String> { route -> val observer = Observer<String> { route ->
if (route.isNotEmpty()) { if (route.isNotEmpty()) {
navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route) routeModel.startNavigation(route)
surfaceRenderer.setRouteData() surfaceRenderer.setRouteData()
recentPlaceActive = false
invalidate() invalidate()
} }
} }
@@ -73,26 +65,48 @@ class NavigationScreen(
val recentObserver = Observer<Place> { lastPlace -> val recentObserver = Observer<Place> { lastPlace ->
if (!routeModel.isNavigating()) { if (!routeModel.isNavigating()) {
recentPlace = lastPlace recentPlace = lastPlace
recentPlaceFound = true navigationType = NavigationType.RECENT
invalidate() invalidate()
} }
} }
val placeObserver = Observer<SearchResult> { searchResult ->
val place = Place(
name = searchResult.displayName,
street = searchResult.address.road,
city = searchResult.address.city,
latitude = searchResult.lat.toDouble(),
longitude = searchResult.lon.toDouble(),
category = Constants.CONTACTS,
postalCode = searchResult.address.postcode
)
navigateToPlace(place)
}
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
val speedObserver = Observer<List<Elements>> { cameras ->
speedCameras = cameras
println("Speed cameras ${speedCameras.size}")
}
init { init {
viewModel.route.observe(this, observer) viewModel.route.observe(this, observer)
viewModel.recentPlace.observe(this, recentObserver) viewModel.recentPlace.observe(this, recentObserver)
viewModel.loadRecentPlace(location = surfaceRenderer.lastLocation) viewModel.loadRecentPlace(location = surfaceRenderer.lastLocation)
viewModel.placeLocation.observe(this, placeObserver)
viewModel.speedCameras.observe(this, speedObserver)
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val actionStripBuilder = createActionStripBuilder() val actionStripBuilder = createActionStripBuilder()
if (calculateNewRoute) { return when (navigationType) {
return navigationRerouteTemplate(actionStripBuilder) NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
} NavigationType.RECENT -> navigationRecentPlaceTemplate()
return if (routeModel.isNavigating()) { NavigationType.REROUTE -> navigationRerouteTemplate(actionStripBuilder)
navigationTemplate(actionStripBuilder) NavigationType.ARRIVAL -> navigationEndTemplate(actionStripBuilder)
} else { else -> navigationViewTemplate(actionStripBuilder)
navigationEndTemplate(actionStripBuilder)
} }
} }
@@ -111,29 +125,36 @@ class NavigationScreen(
.build() .build()
} }
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.build()
}
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.routeState.arrived) { if (routeModel.routeState.arrived) {
val timer = object : CountDownTimer(10000, 10000) { val timer = object : CountDownTimer(8000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
routeModel.routeState = routeModel.routeState.copy(arrived = false) routeModel.routeState = routeModel.routeState.copy(arrived = false)
navigationType = NavigationType.VIEW
invalidate() invalidate()
} }
} }
timer.start() timer.start()
return navigationArrivedTemplate(actionStripBuilder) return navigationArrivedTemplate(actionStripBuilder)
} else { } else {
return if (recentPlaceFound && recentPlaceActive) { return NavigationTemplate.Builder()
return recentPlaceTemplate()
} else {
NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY) .setBackgroundColor(CarColor.SECONDARY)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build()) .setMapActionStrip(mapActionStripBuilder().build())
.build() .build()
} }
} }
}
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = "" var street = ""
@@ -163,7 +184,7 @@ class NavigationScreen(
.build() .build()
} }
fun recentPlaceTemplate(): Template { fun navigationRecentPlaceTemplate(): Template {
val messageTemplate = MessageTemplate.Builder( val messageTemplate = MessageTemplate.Builder(
recentPlace.name + "\n" recentPlace.name + "\n"
+ recentPlace.city + recentPlace.city
@@ -266,6 +287,7 @@ class NavigationScreen(
} }
private fun navigateAction(): Action { private fun navigateAction(): Action {
navigationType = NavigationType.NAVIGATION
return Action.Builder() return Action.Builder()
.setIcon( .setIcon(
CarIcon.Builder( CarIcon.Builder(
@@ -296,9 +318,10 @@ class NavigationScreen(
.build() .build()
) )
.setOnClickListener { .setOnClickListener {
recentPlaceActive = false navigationType = NavigationType.VIEW
invalidate() invalidate()
} }
.setFlags(FLAG_DEFAULT)
.build() .build()
} }
@@ -368,28 +391,6 @@ class NavigationScreen(
.build() .build()
} }
private fun getSuggestion(title: Int, subtitle: Int, icon: CarIcon): Suggestion {
return Suggestion.Builder()
.setIdentifier("0")
.setTitle(carContext.getString(title))
.setSubtitle(carContext.getString(subtitle))
.setIcon(icon)
.setAction(
CarPendingIntent.getCarApp(
carContext, 0,
Intent().setComponent(
ComponentName(
carContext,
NavigationCarAppService::class.java
)
),
//.setAction(NavigationSession.EXECUTE_SCRIPT),
0
)
)
.build()
}
private fun startSearchScreen() { private fun startSearchScreen() {
screenManager screenManager
.pushForResult( .pushForResult(
@@ -397,26 +398,39 @@ class NavigationScreen(
) { obj: Any? -> ) { obj: Any? ->
if (obj != null) { if (obj != null) {
val place = obj as Place val place = obj as Place
val location = Location(LocationManager.GPS_PROVIDER) if (place.longitude == 0.0) {
location.latitude = place.latitude viewModel.findAddress(
location.longitude = place.longitude "${obj.city} ${obj.street}},",
viewModel.saveRecent(place) currentNavigationLocation
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location) )
currentNavigationLocation = location // result see observer
routeModel.routeState.destination = place } else {
invalidate() navigateToPlace(place)
}
} }
} }
} }
fun navigateToPlace(place: Place) {
navigationType = NavigationType.VIEW
val location = location(place.longitude, place.latitude)
viewModel.saveRecent(place)
currentNavigationLocation = location
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
routeModel.routeState.destination = place
invalidate()
}
fun stopNavigation() { fun stopNavigation() {
navigationType = NavigationType.VIEW
listener.stopNavigation() listener.stopNavigation()
surfaceRenderer.routeData.value = "" surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
invalidate() invalidate()
} }
fun calculateNewRoute(destination: Place) { fun calculateNewRoute(destination: Place) {
calculateNewRoute = true navigationType = NavigationType.REROUTE
stopNavigation() stopNavigation()
invalidate() invalidate()
val mainThreadHandler = Handler(carContext.mainLooper) val mainThreadHandler = Handler(carContext.mainLooper)
@@ -424,7 +438,7 @@ class NavigationScreen(
object : CountDownTimer(3000, 1000) { object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
calculateNewRoute = false navigationType = NavigationType.NAVIGATION
reRoute(destination) reRoute(destination)
} }
}.start() }.start()
@@ -437,6 +451,12 @@ class NavigationScreen(
} }
fun updateTrip(location: Location) { fun updateTrip(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
viewModel.getSpeedCameras(location)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
with(routeModel) { with(routeModel) {
updateLocation(location) updateLocation(location)
if (routeState.maneuverType == Maneuver.TYPE_DESTINATION if (routeState.maneuverType == Maneuver.TYPE_DESTINATION
@@ -445,8 +465,33 @@ class NavigationScreen(
stopNavigation() stopNavigation()
routeState = routeState.copy(arrived = true) routeState = routeState.copy(arrived = true)
surfaceRenderer.routeData.value = "" surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()
} }
} }
invalidate() invalidate()
} }
private fun updateDistance(
location: Location,
) {
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon!!, latitude = it.lat!!)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
updatedCameras.add(it)
}
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.first()
if (camera.distance < 100) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
enum class NavigationType {
VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL
} }

View File

@@ -7,6 +7,7 @@ import android.text.SpannableString
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast 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.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
@@ -22,10 +23,14 @@ 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.CONTACTS
import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Constants.categories 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
import kotlin.math.min
class PlaceListScreen( class PlaceListScreen(
@@ -62,37 +67,24 @@ class PlaceListScreen(
} }
fun loadPlaces() { fun loadPlaces() {
if (category == Constants.RECENT) { if (category == RECENT) {
viewModel.loadRecentPlaces(carContext, location) viewModel.loadRecentPlaces(carContext, location)
} }
if (category == Constants.CONTACTS) { if (category == CONTACTS) {
viewModel.loadContacts(carContext, location) viewModel.loadContacts(carContext)
} }
if (category == Constants.FAVORITES) { if (category == FAVORITES) {
viewModel.loadFavorites(carContext, location) viewModel.loadFavorites(carContext, location)
} }
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
.setNoItemsMessage(carContext.getString(R.string.no_places)) .setNoItemsMessage(carContext.getString(R.string.no_places))
places.forEach { places.forEach {
itemListBuilder.addItem( val row = Row.Builder()
Row.Builder()
.addAction(
deleteAction(it)
)
.setImage(contactIcon(it.avatar, it.category)) .setImage(contactIcon(it.avatar, it.category))
.setTitle(it.name!!) .setTitle(it.name!!)
.addText(SpannableString(" ").apply {
setSpan(
DistanceSpan.create(
Distance.create(
it.distance.toDouble(),
Distance.UNIT_KILOMETERS
)
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
})
.setOnClickListener { .setOnClickListener {
val place = Place( val place = Place(
0, 0,
@@ -105,30 +97,44 @@ class PlaceListScreen(
it.street, it.street,
avatar = null avatar = null
) )
setResult(place) screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
surfaceRenderer,
place
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish() finish()
// screenManager
// .pushForResult(
// RoutePreviewScreen(
// carContext,
// surfaceRenderer,
// place
// )
// ) { obj: Any? ->
// if (obj != null) {
// setResult(obj)
// finish()
// }
// }
} }
.build() }
}
if (category != CONTACTS) {
row.addText(SpannableString(" ").apply {
setSpan(
DistanceSpan.create(
Distance.create(
it.distance.toDouble(),
Distance.UNIT_KILOMETERS
)
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
)
})
row.addAction(
deleteAction(it)
)
}
itemListBuilder.addItem(
row.build()
) )
} }
var title = "" var title = ""
when (category) { when (category) {
Constants.RECENT -> title = carContext.getString(R.string.recent_destinations) RECENT -> title = carContext.getString(R.string.recent_destinations)
Constants.CONTACTS -> title = carContext.getString(R.string.contacts) CONTACTS -> title = carContext.getString(R.string.contacts)
Constants.FAVORITES -> title = carContext.getString(R.string.favorites) FAVORITES -> title = carContext.getString(R.string.favorites)
} }
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)

View File

@@ -1,18 +1,3 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.os.CountDownTimer import android.os.CountDownTimer
@@ -22,18 +7,23 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.Action.FLAG_PRIMARY 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.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
import androidx.car.app.model.DurationSpan import androidx.car.app.model.DurationSpan
import androidx.car.app.model.Header import androidx.car.app.model.Header
import androidx.car.app.model.ItemList import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate import androidx.car.app.model.ListTemplate
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Row import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.kouros.data.R import com.kouros.data.R
@@ -78,24 +68,20 @@ class RoutePreviewScreen(
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val navigateActionIcon: CarIcon = CarIcon.Builder( val navigateActionIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource( IconCompat.createWithResource(
carContext, R.drawable.baseline_assistant_navigation_24 carContext, R.drawable.navigation_48px
) )
).build() ).build()
val navigateAction = Action.Builder() val navigateAction = Action.Builder()
.setFlags(FLAG_PRIMARY) .setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon) .setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate() } .setOnClickListener { this.onNavigate() }
.build() .build()
val itemListBuilder = ItemList.Builder()
if (routeModel.isNavigating() && routeModel.route.waypoints.isNotEmpty()) {
itemListBuilder.addItem(createRow(0, navigateAction))
}
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.route_preview)) .setTitle(carContext.getString(R.string.route_preview))
//.addEndHeaderAction(navigateAction)
.addEndHeaderAction( .addEndHeaderAction(
favoriteAction() favoriteAction()
) )
@@ -104,21 +90,30 @@ class RoutePreviewScreen(
) )
.build() .build()
val timer = object : CountDownTimer(10000, 15000) { val message = if (routeModel.isNavigating() && routeModel.route.waypoints.isNotEmpty()) {
createRouteText()
} else {
CarText.Builder("Wait")
.build()
}
val messageTemplate = MessageTemplate.Builder(
message
)
.setHeader(header)
.addAction(navigateAction)
.setLoading(message.toString() == "Wait")
.build()
val timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
onNavigate() //onNavigate()
} }
} }
timer.start() timer.start()
return MapWithContentTemplate.Builder() return MapWithContentTemplate.Builder()
.setContentTemplate( .setContentTemplate(messageTemplate)
ListTemplate.Builder()
.setHeader(header)
.setSingleList(itemListBuilder.build())
.build()
)
.setMapController( .setMapController(
MapController.Builder().setMapActionStrip( MapController.Builder().setMapActionStrip(
getMapActionStrip() getMapActionStrip()
@@ -177,18 +172,8 @@ class RoutePreviewScreen(
.build() .build()
) )
.build() .build()
private fun createRow(index: Int, action: Action): Row {
val route: CarText = createRouteText(index)
return Row.Builder()
.setTitle(route)
.setOnClickListener { onRouteSelected(index) }
.addText("${destination.street!!} ${destination.postalCode} ${destination.city}")
.addAction(action)
.build()
}
private fun createRouteText(): CarText {
private fun createRouteText(index: Int): CarText {
val time = routeModel.route.summary.time val time = routeModel.route.summary.time
val length = BigDecimal(routeModel.route.distance).setScale(1, RoundingMode.HALF_EVEN) val length = BigDecimal(routeModel.route.distance).setScale(1, RoundingMode.HALF_EVEN)
val firstRoute = SpannableString(" \u00b7 $length km") val firstRoute = SpannableString(" \u00b7 $length km")

View File

@@ -35,7 +35,7 @@ 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.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))
) )

View File

@@ -31,7 +31,6 @@ class SettingsScreen(
) : Screen(carContext) { ) : Screen(carContext) {
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val listBuilder = ItemList.Builder() val listBuilder = ItemList.Builder()
listBuilder.addItem( listBuilder.addItem(
buildRowForTemplate( buildRowForTemplate(

View File

@@ -24,9 +24,11 @@ class ViewModelTest {
fun routeViewModelTest() { fun routeViewModelTest() {
val fromLocation = Location(LocationManager.GPS_PROVIDER) val fromLocation = Location(LocationManager.GPS_PROVIDER)
fromLocation.isMock = true
fromLocation.latitude = homeLocation.latitude fromLocation.latitude = homeLocation.latitude
fromLocation.longitude = homeLocation.longitude fromLocation.longitude = homeLocation.longitude
val toLocation = Location(LocationManager.GPS_PROVIDER) val toLocation = Location(LocationManager.GPS_PROVIDER)
toLocation.isMock = true
toLocation.latitude = home2Location.latitude toLocation.latitude = home2Location.latitude
toLocation.longitude = home2Location.longitude toLocation.longitude = home2Location.longitude

View File

@@ -9,6 +9,7 @@ data class Elements (
@SerializedName("id" ) var id : Long? = null, @SerializedName("id" ) var id : Long? = null,
@SerializedName("lat" ) var lat : Double? = null, @SerializedName("lat" ) var lat : Double? = null,
@SerializedName("lon" ) var lon : Double? = null, @SerializedName("lon" ) var lon : Double? = null,
@SerializedName("tags" ) var tags : Tags = Tags() @SerializedName("tags" ) var tags : Tags = Tags(),
var distance : Double = 0.0
) )

View File

@@ -12,21 +12,22 @@ import java.net.URL
class Overpass { 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(type: String, category: String, location: Location) : List<Elements> {
val boundingBox = getOverpassBbox(location, 2.0) val boundingBox = getOverpassBbox(location, 5.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(
"Accept", "Accept",
"application/json" "application/json"
) )
// node["highway"="speed_camera"]
// node[amenity=$category]
httpURLConnection.setDoOutput(true); httpURLConnection.setDoOutput(true);
// define a query // define search query
val test = """ val searchQuery = """
|[out:json]; |[out:json];
|( |(
| node[amenity=$category] | node[$type=$category]
| ($boundingBox); | ($boundingBox);
|); |);
|out body; |out body;
@@ -34,7 +35,7 @@ class Overpass {
// Send the JSON we created // Send the JSON we created
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream) val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
outputStreamWriter.write(test) outputStreamWriter.write(searchQuery)
outputStreamWriter.flush() outputStreamWriter.flush()
// Check if the connection is successful // Check if the connection is successful
val responseCode = httpURLConnection.responseCode val responseCode = httpURLConnection.responseCode
@@ -43,7 +44,7 @@ class Overpass {
.use { it.readText() } // defaults to UTF-8 .use { it.readText() } // defaults to UTF-8
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
val overpass = gson.fromJson(response, Amenity::class.java) val overpass = gson.fromJson(response, Amenity::class.java)
println("Overpass: $response") println("Overpass: $type $response")
return overpass.elements return overpass.elements
} }
return emptyList() return emptyList()

View File

@@ -4,7 +4,7 @@ import com.google.gson.annotations.SerializedName
data class Tags( data class Tags(
@SerializedName("name") var name: String? = null,
@SerializedName("amenity") var amenity: String? = null, @SerializedName("amenity") var amenity: String? = null,
@SerializedName("authentication:none") var authenticationNone: String? = null, @SerializedName("authentication:none") var authenticationNone: String? = null,
@SerializedName("capacity") var capacity: String? = null, @SerializedName("capacity") var capacity: String? = null,
@@ -17,6 +17,8 @@ data class Tags (
@SerializedName("operator:wikipedia") var operatorWikipedia: String? = null, @SerializedName("operator:wikipedia") var operatorWikipedia: String? = null,
@SerializedName("ref") var ref: String? = null, @SerializedName("ref") var ref: String? = null,
@SerializedName("socket:type2") var socketType2: String? = null, @SerializedName("socket:type2") var socketType2: String? = null,
@SerializedName("socket:type2:output" ) var socketType2Output : String? = null @SerializedName("socket:type2:output") var socketType2Output: String? = null,
@SerializedName("maxspeed") var maxspeed: String? = null,
@SerializedName("direction") var direction: String? = null,
) )

View File

@@ -36,7 +36,7 @@ class Contacts(private var context: Context) {
if (name.contains("Jola") if (name.contains("Jola")
|| name.contains("Dominic") || name.contains("Dominic")
|| name.contains("Martha") || name.contains("Martha")
|| name.contains("Rena") || name.contains("Groth")
|| name.contains("David")) { || name.contains("David")) {
val mimeType: String = getString(getColumnIndex(ContactsContract.Data.MIMETYPE)) val mimeType: String = getString(getColumnIndex(ContactsContract.Data.MIMETYPE))
if (mimeType == ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) { if (mimeType == ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) {

View File

@@ -127,6 +127,7 @@ open class RouteModel() {
maneuverType = relevantManeuver.type maneuverType = relevantManeuver.type
} }
val maneuverIconPair = maneuverIcon(maneuverType) val maneuverIconPair = maneuverIcon(maneuverType)
routeState.maneuverType = maneuverIconPair.first
// Construct and return the final StepData object // Construct and return the final StepData object
return StepData( return StepData(
streetName, streetName,
@@ -138,40 +139,7 @@ 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
@@ -181,7 +149,6 @@ open class RouteModel() {
when (distanceLeft) { when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> { in 0.0..NEXT_STEP_THRESHOLD -> {
} }
else -> { else -> {
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) { if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0] text = maneuver.streetNames[0]
@@ -319,7 +286,7 @@ open class RouteModel() {
currentTurnIcon = R.drawable.ic_roundabout_ccw currentTurnIcon = R.drawable.ic_roundabout_ccw
} }
} }
routeState.maneuverType = type //routeState.maneuverType = type
return Pair(type, currentTurnIcon) return Pair(type, currentTurnIcon)
} }
@@ -331,7 +298,7 @@ open class RouteModel() {
fun hasArrived(type: Int): Boolean { fun hasArrived(type: Int): Boolean {
// return leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE && type == ManeuverType.DestinationRight.value // return leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE && type == ManeuverType.DestinationRight.value
return type == ManeuverType.DestinationRight.value return type == ManeuverType.DestinationRight.value
|| routeState.maneuverType == ManeuverType.Destination.value || type == ManeuverType.Destination.value
|| routeState.maneuverType == ManeuverType.DestinationLeft.value || type == ManeuverType.DestinationLeft.value
} }
} }

View File

@@ -1,7 +1,6 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.content.Context import android.content.Context
import android.location.Geocoder
import android.location.Location import android.location.Location
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
@@ -10,16 +9,15 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
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.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.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass
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
@@ -31,34 +29,42 @@ import java.time.ZoneOffset
class ViewModel(private val repository: NavigationRepository) : ViewModel() { class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val route: MutableLiveData<String> by lazy { val route: MutableLiveData<String> by lazy {
MutableLiveData<String>() MutableLiveData()
} }
val previewRoute: MutableLiveData<String> by lazy { val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData<String>() MutableLiveData()
} }
val recentPlace: MutableLiveData<Place> by lazy { val recentPlace: MutableLiveData<Place> by lazy {
MutableLiveData<Place>() MutableLiveData()
} }
val places: MutableLiveData<List<Place>> by lazy { val places: MutableLiveData<List<Place>> by lazy {
MutableLiveData<List<Place>>() MutableLiveData()
} }
val favorites: MutableLiveData<List<Place>> by lazy { val favorites: MutableLiveData<List<Place>> by lazy {
MutableLiveData<List<Place>>() MutableLiveData()
} }
val searchPlaces: MutableLiveData<List<SearchResult>> by lazy { val searchPlaces: MutableLiveData<List<SearchResult>> by lazy {
MutableLiveData<List<SearchResult>>() MutableLiveData()
}
val placeLocation: MutableLiveData<SearchResult> by lazy {
MutableLiveData()
} }
val contactAddress: MutableLiveData<List<Place>> by lazy { val contactAddress: MutableLiveData<List<Place>> by lazy {
MutableLiveData<List<Place>>() MutableLiveData()
} }
val elements: MutableLiveData<List<Elements>> by lazy { val elements: MutableLiveData<List<Elements>> by lazy {
MutableLiveData<List<Elements>>() MutableLiveData()
}
val speedCameras: MutableLiveData<List<Elements>> by lazy {
MutableLiveData()
} }
fun loadRecentPlace(location: Location) { fun loadRecentPlace(location: Location) {
@@ -98,10 +104,16 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
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)
if (place.latitude != 0.0) {
val distance = val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context)) repository.getRouteDistance(
location,
plLocation,
getSearchFilter(context)
)
place.distance = distance.toFloat() place.distance = distance.toFloat()
} }
}
places.postValue(results) places.postValue(results)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -164,27 +176,14 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
} }
} }
fun loadContacts(context: Context, currentLocation: Location) { fun loadContacts(context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try {
val geocoder = Geocoder(context)
val contactList = mutableListOf<Place>() val contactList = mutableListOf<Place>()
val contacts = Contacts(context = context) val contacts = Contacts(context = context)
val addresses = contacts.retrieveContacts() val addresses = contacts.retrieveContacts()
for (address in addresses) { for (address in addresses) {
val addressLines = address.address.split("\n") val addressLines = address.address.split("\n")
geocoder.getFromLocationName(
address.address, 5
) {
for (adr in it) {
if (addressLines.size > 1) { if (addressLines.size > 1) {
val plLocation = location(adr.longitude, adr.latitude)
val distance =
repository.getRouteDistance(
currentLocation,
plLocation,
getSearchFilter(context)
)
contactList.add( contactList.add(
Place( Place(
id = address.contactId, id = address.contactId,
@@ -192,24 +191,39 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
Constants.CONTACTS, Constants.CONTACTS,
street = addressLines[0], street = addressLines[0],
city = addressLines[1], city = addressLines[1],
latitude = adr.latitude,
longitude = adr.longitude,
avatar = address.avatar, avatar = address.avatar,
distance = distance.toFloat() longitude = 0.0,
latitude = 0.0,
distance = 0F,
) )
) )
} }
}
contactAddress.postValue(contactList) contactAddress.postValue(contactList)
} }
} }
} catch (e: Exception) {
e.printStackTrace()
}
}
} }
fun findAddress(search: String, location: Location) {
var sortedList: List<SearchResult>
viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location)
val gson = GsonBuilder().serializeNulls().create()
val places = gson.fromJson(placesJson, Search::class.java)
val distPlaces = mutableListOf<SearchResult>()
places.forEach {
val plLocation =
location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble())
val distance = plLocation.distanceTo(location)
it.distance = distance
distPlaces.add(it)
}
sortedList = distPlaces.sortedWith(compareBy { it.distance })
if (sortedList.isNotEmpty()) {
placeLocation.postValue(sortedList.first())
}
}
}
fun searchPlaces(search: String, location: Location) { fun searchPlaces(search: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location) val placesJson = repository.searchPlaces(search, location)
@@ -223,7 +237,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
it.distance = distance it.distance = distance
distPlaces.add(it) distPlaces.add(it)
} }
val sortedList = distPlaces.sortedWith(compareBy({ it.distance })) val sortedList = distPlaces.sortedWith(compareBy { it.distance })
searchPlaces.postValue(sortedList) searchPlaces.postValue(sortedList)
} }
} }
@@ -237,8 +251,33 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun getAmenities(category: String, location: Location) { fun getAmenities(category: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities(category, location) val amenities = Overpass().getAmenities("amenity", category, location)
elements.postValue(amenities) val distAmenities = mutableListOf<Elements>()
amenities.forEach {
val plLocation =
location(longitude = it.lon!!, latitude = it.lat!!)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
distAmenities.add(it)
}
val sortedList = distAmenities.sortedWith(compareBy { it.distance })
elements.postValue(sortedList)
}
}
fun getSpeedCameras(location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location)
val distAmenities = mutableListOf<Elements>()
amenities.forEach {
val plLocation =
location(longitude = it.lon!!, latitude = it.lat!!)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
distAmenities.add(it)
}
val sortedList = distAmenities.sortedWith(compareBy { it.distance })
speedCameras.postValue(sortedList)
} }
} }

View File

@@ -2,6 +2,8 @@ package com.kouros.navigation.utils
import android.location.Location import android.location.Location
import com.kouros.navigation.data.BoundingBox import com.kouros.navigation.data.BoundingBox
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
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.Feature
@@ -102,12 +104,12 @@ object GeoUtils {
return featureCollection.toJson() return featureCollection.toJson()
} }
fun createPointCollection(lineCoordinates: List<List<Double>>): String { fun createPointCollection(lineCoordinates: List<List<Double>>, category: String): String {
val featureCollection = buildFeatureCollection { val featureCollection = buildFeatureCollection {
lineCoordinates.forEach { lineCoordinates.forEach {
addFeature { addFeature {
geometry = org.maplibre.spatialk.geojson.Point(it[0], it[1]) geometry = org.maplibre.spatialk.geojson.Point(it[0], it[1])
properties = null properties = buildJsonObject { put("category", category) }
} }
} }
} }

View File

@@ -4,19 +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 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 java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -24,7 +12,6 @@ import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.Duration import kotlin.time.Duration
@@ -88,9 +75,8 @@ fun calculateZoom(speed: Double?): Double {
val zoom = when (speedKmh) { val zoom = when (speedKmh) {
in 0..10 -> 18.0 in 0..10 -> 18.0
in 11..30 -> 17.0 in 11..30 -> 17.0
in 21..40 -> 16.0 in 31..50 -> 16.0
in 31..50 -> 15.0 in 61..70 -> 15.0
in 51..60 -> 15.0
else -> 14 else -> 14
} }
return zoom.toDouble() return zoom.toDouble()
@@ -98,21 +84,15 @@ fun calculateZoom(speed: Double?): Double {
fun previewZoom(previewDistance: Double): Double { fun previewZoom(previewDistance: Double): Double {
when (previewDistance) { when (previewDistance) {
in 0.0..10.0 -> { in 0.0..10.0 -> return 13.0
return 13.0 in 10.0..20.0 -> return 11.0
} in 20.0..30.0 -> return 10.0
in 10.0..20.0 -> {
return 11.0
}
in 20.0..30.0 -> {
return 10.0
}
} }
return 9.0 return 9.0
} }
fun calculateTilt(newZoom: Double, tilt: Double): Double =
fun calcTilt(newZoom: Double, tilt: Double): Double = if (newZoom < 13) { if (newZoom < 13) {
0.0 0.0
} else { } else {
if (tilt == 0.0) { if (tilt == 0.0) {
@@ -121,6 +101,7 @@ fun calcTilt(newZoom: Double, tilt: Double): Double = if (newZoom < 13) {
tilt 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) {

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.71,468 649.36,482.64Q664,497.29 664,518L664,737Q664,759 681.5,773.5Q699,788 722,788Q745,788 765,773.5Q785,759 785,737L785,350L770,350Q757.25,350 748.63,341.37Q740,332.75 740,320L740,230L760,230L760,180L790,180L790,230L830,230L830,180L860,180L860,230L880,230L880,320Q880,332.75 871.38,341.37Q862.75,350 850,350L835,350L835,736.69Q835,780 801,810Q767,840 721.82,840Q677.66,840 645.83,810Q614,780 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM337,746L425,606L372,606L372,501L285,641L337,641L337,746Z"/>
</vector>

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="M160,840L160,200Q160,167 183.5,143.5Q207,120 240,120L480,120Q513,120 536.5,143.5Q560,167 560,200L560,480L600,480Q633,480 656.5,503.5Q680,527 680,560L680,740Q680,757 691.5,768.5Q703,780 720,780Q737,780 748.5,768.5Q760,757 760,740L760,452Q751,457 741,458.5Q731,460 720,460Q678,460 649,431Q620,402 620,360Q620,328 637.5,302.5Q655,277 684,266L600,182L642,140L790,284Q805,299 812.5,319Q820,339 820,360L820,740Q820,782 791,811Q762,840 720,840Q678,840 649,811Q620,782 620,740L620,540Q620,540 620,540Q620,540 620,540L560,540L560,840L160,840ZM240,400L480,400L480,200Q480,200 480,200Q480,200 480,200L240,200Q240,200 240,200Q240,200 240,200L240,400ZM720,400Q737,400 748.5,388.5Q760,377 760,360Q760,343 748.5,331.5Q737,320 720,320Q703,320 691.5,331.5Q680,343 680,360Q680,377 691.5,388.5Q703,400 720,400ZM240,760L480,760L480,480L240,480L240,760ZM480,760L240,760L240,760L480,760Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.63,468 649.31,482.69Q664,497.37 664,518L664,737Q664,758.68 679.5,773.34Q695,788 717,788Q739,788 754.5,773.34Q770,758.68 770,737L770,442Q759,448 747,451Q735,454 723,454Q683.52,454 656.26,426.74Q629,399.48 629,360Q629,328.39 647,303.19Q665,278 695,270L600,175L636,140L789,293Q803,307 811.5,323.5Q820,340 820,360L820,737Q820,780.26 790.18,810.13Q760.37,840 717.18,840Q674,840 644,810.13Q614,780.26 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM723,404Q741,404 754,391Q767,378 767,360Q767,342 754,329Q741,316 723,316Q705,316 692,329Q679,342 679,360Q679,378 692,391Q705,404 723,404Z"/>
</vector>

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="M120,840L120,760L200,520L120,280L120,200L628,200L686,40L780,74L734,200L840,200L840,280L760,520L840,760L840,840L120,840ZM440,680L520,680L520,560L640,560L640,480L520,480L520,360L440,360L440,480L320,480L320,560L440,560L440,680ZM204,760L756,760L676,520L756,280L204,280L284,520L204,760ZM480,520L480,520L480,520L480,520L480,520L480,520Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,840L120,780L207,525L120,270L120,210L647,210L709,40L777,67L725,210L840,210L840,270L752,525L840,780L840,840L120,840ZM452,679L512,679L512,555L636,555L636,495L512,495L512,371L452,371L452,495L328,495L328,555L452,555L452,679Z"/>
</vector>

View File

@@ -6,7 +6,7 @@ koinAndroid = "4.1.1"
koinAndroidxCompose = "4.1.1" koinAndroidxCompose = "4.1.1"
koinComposeViewmodel = "4.1.1" koinComposeViewmodel = "4.1.1"
koinCore = "4.1.1" koinCore = "4.1.1"
kotlin = "2.2.21" kotlin = "2.3.0"
coreKtx = "1.17.0" coreKtx = "1.17.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"