Compare commits

..

12 Commits

Author SHA1 Message Date
Dimitris
65ff41995d TomTom Routing 2026-02-10 10:50:42 +01:00
Dimitris
5141041b5c TomTom Routing 2026-02-09 13:36:05 +01:00
Dimitris
e9474695bf TomTom Routing 2026-02-09 08:44:57 +01:00
Dimitris
0d51c6121d TomTom Routing 2026-02-07 12:56:45 +01:00
Dimitris
eac5b56bcb Before TomTom Routing 2026-01-29 12:13:37 +01:00
Dimitris
7db7cba4fb Lanes 2026-01-20 13:21:58 +01:00
Dimitris
e22865bd73 Lanes 2026-01-12 13:12:22 +01:00
Dimitris
e274011080 Diverse 2026-01-10 12:48:41 +01:00
Dimitris
7efa2685be Lanes 2026-01-06 08:25:27 +01:00
Dimitris
fdf2ee9f48 CarInfo and CarSensors Osrm 2026-01-03 14:04:50 +01:00
Dimitris
1eab4f1aa3 CarInfo and CarSensors 2025-12-31 11:16:41 +01:00
Dimitris
9f356bd728 Osrm 2025-12-30 16:11:30 +01:00
124 changed files with 18874 additions and 2434 deletions

200
CLAUDE.md Normal file
View File

@@ -0,0 +1,200 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is an Android navigation app built with Jetpack Compose that supports multiple routing providers (OSRM, Valhalla, TomTom) and includes Android Auto/Automotive OS integration. The app uses MapLibre for rendering, ObjectBox for local persistence, and Koin for dependency injection.
## Build Commands
```bash
# Build the app (from repository root)
./gradlew :app:assembleDebug
# Build specific flavor
./gradlew :app:assembleDemoDebug
./gradlew :app:assembleFullDebug
# Run tests
./gradlew test
# Run tests for specific module
./gradlew :common:data:test
./gradlew :common:car:test
# Install on device
./gradlew :app:installDebug
# Clean build
./gradlew clean
```
## Module Structure
The project uses a multi-module architecture:
- **app/** - Main Android app with Jetpack Compose UI for phone
- **common/data/** - Core data layer with routing logic, repositories, and data models (shared by all modules)
- **common/car/** - Android Auto/Automotive OS UI implementation
- **automotive/** - Placeholder for future native Automotive OS app
Dependencies flow: `app``common:car``common:data`
## Architecture
### Routing Providers (Pluggable System)
The app supports three routing engines that implement the `NavigationRepository` abstract class:
1. **OsrmRepository** - OSRM routing engine
2. **ValhallaRepository** - Valhalla routing engine
3. **TomTomRepository** - TomTom routing engine
Each provider has a corresponding mapper class (`OsrmRoute`, `ValhallaRoute`, `TomTomRoute`) that converts provider-specific JSON responses to the universal `Route` data model.
**Adding a new routing provider:**
1. Create `NewProviderRepository` extending `NavigationRepository` in `common/data/src/main/java/com/kouros/navigation/data/`
2. Implement `getRoute()` method
3. Create `NewProviderRoute.kt` with `mapToRoute()` function
4. Add provider detection logic in `Route.Builder.route()`
5. Update `NavigationUtils.getViewModel()` to return appropriate ViewModel
### Data Flow
```
User Action (search/select destination)
ViewModel.loadRoute() [LiveData]
NavigationRepository.getRoute() [Selected provider]
*Route.mapToRoute() [Convert to universal Route model]
RouteModel.startNavigation()
RouteModel.updateLocation() [On each location update]
UI observes LiveData and displays current step
```
### Key Classes
**Navigation Logic:**
- `RouteModel.kt` - Core navigation engine (tracks position, calculates distances, manages steps)
- `RouteCarModel.kt` - Extends RouteModel with Android Auto-specific formatting
- `ViewModel.kt` - androidx.ViewModel with LiveData for route, traffic, places, etc.
**Data Models:**
- `Route.kt` - Universal route structure used by all providers
- `Place.kt` - ObjectBox entity for favorites/recent locations
- `StepData.kt` - Display data for current navigation instruction
**Repositories:**
- `NavigationRepository.kt` - Abstract base class for all routing providers
- Also handles Nominatim geocoding search and TomTom traffic incidents
**Android Auto:**
- `NavigationCarAppService.kt` - Entry point for Android Auto/Automotive OS
- `NavigationSession.kt` - Session management
- `NavigationScreen.kt` - Car screen templates with NavigationType state machine
- `SurfaceRenderer.kt` - Handles virtual display and map rendering
### External APIs
| Service | Purpose | Base URL |
|---------|---------|----------|
| OSRM | Routing | `https://kouros-online.de/osrm/route/v1/driving/` |
| Valhalla | Routing | `https://kouros-online.de/valhalla/route` |
| TomTom | Traffic incidents | `https://api.tomtom.com/traffic/services/5/incidentDetails` |
| Nominatim | Geocoding search | `https://kouros-online.de/nominatim/` |
| Overpass | POI & speed limits | OpenStreetMap Overpass API |
## Important Constants
Located in `Constants.kt` (`common/data`):
```kotlin
NEXT_STEP_THRESHOLD = 120.0 m // Distance to show next maneuver
DESTINATION_ARRIVAL_DISTANCE = 40.0 m // Distance to trigger arrival
MAXIMAL_SNAP_CORRECTION = 50.0 m // Max distance to snap to route
MAXIMAL_ROUTE_DEVIATION = 80.0 m // Max deviation before reroute
```
SharedPreferences keys:
- `ROUTING_ENGINE` - Selected provider (0=Valhalla, 1=OSRM, 2=TomTom)
- `DARK_MODE_SETTINGS` - Theme preference
- `AVOID_MOTORWAY`, `AVOID_TOLLWAY` - Route preferences
## Navigation Flow
1. **Route Loading**: User searches via Nominatim → selects place → ViewModel.loadRoute() calls selected repository
2. **Route Parsing**: Provider JSON → mapper converts to universal Route → RouteModel.startNavigation()
3. **Location Tracking**: FusedLocationProviderClient provides updates → RouteModel.updateLocation()
4. **Step Calculation**: findStep() snaps location to nearest waypoint → updates current step
5. **UI Updates**: currentStep() and nextStep() provide display data (instruction, distance, icon, lanes)
6. **Arrival**: When distance < DESTINATION_ARRIVAL_DISTANCE, navigation ends
## Testing Navigation
The app includes mock location support for testing:
- Set `useMock = true` in MainActivity
- Enable "Mock location app" in Android Developer Options
- Choose test mode:
- `type = 1` - Simulate movement along entire route
- `type = 2` - Test specific step range
- `type = 3` - Replay GPX track file
## ObjectBox Database
ObjectBox is configured in `common/data/build.gradle.kts` with the kapt plugin. The database stores:
- Recent destinations (category: "Recent")
- Favorite places (category: "Favorites")
- Imported contacts (category: "Contacts")
Queries use ObjectBox query builder pattern with generated `Place_` property accessors.
## Compose UI Structure
**Phone App:**
- `MainActivity.kt` - Main entry with permission handling and Navigation Compose
- `NavigationScreen.kt` - Turn-by-turn navigation display
- `SearchSheet.kt` / `NavigationSheet.kt` - Bottom sheet content
- `MapView.kt` - MapLibre rendering with camera state management
**Android Auto:**
- Uses CarAppService Screen templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate)
- NavigationType enum controls which template to display (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)
## Build Flavors
Two product flavors with dimension "version":
- **demo** - applicationId: `com.kouros.navigation.demo`
- **full** - applicationId: `com.kouros.navigation.full`
## Common Patterns
**Dependency Injection (Koin):**
```kotlin
single { OsrmRepository() }
viewModel { ViewModel(get()) }
```
**LiveData Observation:**
```kotlin
viewModel.route.observe(this) { routeJson ->
routeModel.startNavigation(routeJson, context)
}
```
**Step Finding Algorithm:**
RouteModel iterates through all step waypoints, calculates distance to current location, and snaps to the nearest waypoint to determine current step index.
## Known Limitations
- Valhalla route mapping is incomplete (search for TODO comments in ValhallaRoute.kt)
- Rerouting logic exists but needs more testing
- Speed limit queries via Overpass API could be optimized for performance
- TomTom implementation uses local JSON file (R.raw.tomom_routing) instead of live API

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 15
versionName = "0.1.3.15"
versionCode = 38
versionName = "0.2.0.38"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -94,7 +94,13 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.window)
implementation(libs.androidx.compose.foundation.layout)
implementation("com.github.ticofab:android-gpx-parser:2.3.1")
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
implementation("com.github.alorma.compose-settings:ui-tiles:2.25.0")
implementation("com.github.alorma.compose-settings:ui-tiles-extended:2.25.0")
implementation("com.github.alorma.compose-settings:ui-tiles-expressive:2.25.0")
implementation(libs.androidx.foundation.layout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -6,7 +6,7 @@
<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_COARSE_LOCATION"/>
<!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>-->
<!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>-->
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="MockLocation" />
@@ -20,6 +20,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Navigation">
<meta-data

View File

@@ -2,25 +2,14 @@ package com.kouros.navigation
import android.app.Application
import android.content.Context
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.di.appModule
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import org.maplibre.compose.expressions.dsl.switch
class MainApplication : Application() {
@@ -28,8 +17,7 @@ class MainApplication : Application() {
super.onCreate()
ObjectBox.init(this);
appContext = applicationContext
setIntKeyValue(appContext!!, RouteEngine.VALHALLA.ordinal, ROUTE_ENGINE)
navigationViewModel = getRouteEngine(appContext!!)
navigationViewModel = getViewModel(appContext!!)
startKoin {
androidLogger(Level.DEBUG)
androidContext(this@MainApplication)
@@ -42,8 +30,6 @@ class MainApplication : Application() {
var appContext: Context? = null
private set
var useContacts = false
lateinit var navigationViewModel : ViewModel
}
}

View File

@@ -2,14 +2,18 @@ package com.kouros.navigation.di
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.ViewModel
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import kotlin.math.sin
val appModule = module {
viewModelOf(::ViewModel)
singleOf(::ValhallaRepository)
singleOf(::OsrmRepository)
}
singleOf(::TomTomRepository)
}

View File

@@ -48,8 +48,8 @@ class MockLocation (private var locationManager: LocationManager) {
this.latitude = latitude
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 10f
this.accuracy = 0f
this.speed = 0f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -71,8 +71,8 @@ class MockLocation (private var locationManager: LocationManager) {
this.latitude = latitude
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 10f
this.accuracy = 0f
this.speed = 0f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()

View File

@@ -0,0 +1,162 @@
package com.kouros.navigation.ui
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.alorma.compose.settings.ui.SettingsCheckbox
import com.alorma.compose.settings.ui.SettingsGroup
import com.alorma.compose.settings.ui.SettingsRadioButton
import com.alorma.compose.settings.ui.base.internal.LocalSettingsTileColors
import com.alorma.compose.settings.ui.base.internal.SettingsTileDefaults
import com.kouros.data.R
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.utils.NavigationUtils
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DisplayScreenSettings(context: Context, navigateBack: () -> Unit) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.display_settings),
)
},
navigationIcon = {
IconButton(onClick = navigateBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
}
},
)
},
) { padding ->
val scrollState = rememberScrollState()
Column(
modifier =
Modifier
.consumeWindowInsets(padding)
.verticalScroll(scrollState)
.padding(top = padding.calculateTopPadding()),
) {
DisplaySettings(context)
}
}
}
@Composable
private fun DisplaySettings(context: Context) {
Section(title = "Anzeige") {
val state = remember {
mutableStateOf(
NavigationUtils.getBooleanKeyValue(
context,
SHOW_THREED_BUILDING
)
)
}
SettingsCheckbox(
state = state.value,
title = { Text(text = stringResource(R.string.threed_building)) },
onCheckedChange = {
state.value = it
NavigationUtils.setBooleanKeyValue(context, it, SHOW_THREED_BUILDING)
},
)
}
Section(title = "Dunkles Design") {
val state = remember {
mutableIntStateOf(
NavigationUtils.getIntKeyValue(
context,
DARK_MODE_SETTINGS
)
)
}
DarkModeData(context).darkDesign.forEach { sampleItem ->
SettingsRadioButton(
state = state.intValue == sampleItem.key,
title = { Text(text = sampleItem.title) },
onClick = {
state.intValue = sampleItem.key
NavigationUtils.setIntKeyValue(context, state.intValue, DARK_MODE_SETTINGS)
},
)
}
}
}
@Composable
internal fun Section(
title: String,
enabled: Boolean = true,
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(4.dp),
content: @Composable ColumnScope.() -> Unit,
) {
SettingsGroup(
contentPadding = PaddingValues(0.dp),
verticalArrangement = verticalArrangement,
enabled = enabled,
title = { Text(text = title) },
) {
Card(
colors = CardDefaults.cardColors(
containerColor = (LocalSettingsTileColors.current
?: SettingsTileDefaults.colors()).containerColor
),
) {
content()
}
}
}
internal class DarkModeData(context: Context) {
val darkDesign =
listOf(
Item(
key = 0,
title = context.getString(R.string.off_action_title),
),
Item(
key = 1,
title = context.getString(R.string.on_action_title),
),
Item(
key = 2,
title = context.getString(R.string.use_telephon_settings),
),
)
}
internal data class Item(
val key: Int,
val title: String,
)

View File

@@ -4,6 +4,7 @@ import NavigationSheet
import android.Manifest
import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.content.Context
import android.location.LocationManager
import android.os.Bundle
import android.os.Process
@@ -14,9 +15,13 @@ import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@@ -32,34 +37,47 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
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.homeVogelhart
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.location
import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.Location
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
@@ -69,7 +87,10 @@ class MainActivity : ComponentActivity() {
val routeModel = RouteModel()
var tilt = 50.0
val useMock = true
val useMock = false
val type = 3 // simulate 2 test 3 gpx 4 testSingle
var currentIndex = 0
val stepData: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>()
}
@@ -80,15 +101,23 @@ class MainActivity : ComponentActivity() {
val observer = Observer<String> { newRoute ->
if (newRoute.isNotEmpty()) {
routeModel.startNavigation(newRoute, applicationContext)
routeData.value = routeModel.route.routeGeoJson
simulate()
//test()
routeData.value = routeModel.curRoute.routeGeoJson
if (useMock) {
when (type) {
1 -> simulate()
2 -> test()
3 -> gpx(
context = applicationContext
)
4 -> testSingle()
}
}
}
}
val cameraPosition = MutableLiveData(
CameraPosition(
zoom = 15.0,
target = Position(latitude = 48.1857475, longitude = 11.5793627)
zoom = 15.0, target = Position(latitude = 48.1857475, longitude = 11.5793627)
)
)
@@ -99,30 +128,28 @@ class MainActivity : ComponentActivity() {
private var loadRecentPlaces = false
private var overpass = false
lateinit var baseStyle: BaseStyle.Json
init {
navigationViewModel.route.observe(this, observer)
}
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val darkModeSettings = getIntKeyValue(applicationContext, Constants.DARK_MODE_SETTINGS)
baseStyle = BaseStyleModel().readStyle(applicationContext, darkModeSettings, false)
if (useMock) {
checkMockLocationEnabled()
}
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.lastLocation
.addOnSuccessListener { location : android.location.Location? ->
if (useMock) {
mock = MockLocation(locationManager)
mock.setMockLocation(
location?.latitude ?: homeLocation.latitude,
location?.longitude ?: homeLocation.longitude
)
}
fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? ->
if (useMock) {
mock = MockLocation(locationManager)
mock.setMockLocation(
homeVogelhart.latitude, homeVogelhart.longitude
)
navigationViewModel.route.observe(this, observer)
}
}
enableEdgeToEdge()
setContent {
CheckPermissionScreen()
@@ -140,23 +167,35 @@ class MainActivity : ComponentActivity() {
permissions = permissions,
requiredPermissions = listOf(permissions.first()),
onGranted = {
Content()
App()
// auto navigate
if (useMock) {
// navigationViewModel.loadRoute(
// applicationContext,
// homeVogelhart,
// homeHohenwaldeck,
// 0F
// )
}
},
)
}
@SuppressLint("AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Content() {
fun StartScreen(
navController: NavHostController
) {
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val sheetPeekHeightState = remember { mutableStateOf(256.dp) }
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
@@ -194,77 +233,108 @@ class MainActivity : ComponentActivity() {
applicationContext,
userLocationState,
step,
nextStep,
cameraPosition,
routeData,
tilt
tilt,
baseStyle
)
}
if (!routeModel.isNavigating()) {
Settings(navController, modifier = Modifier.fillMaxWidth())
}
}
}
}
@Composable
fun App() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "startScreen") {
composable("startScreen") { StartScreen(navController) }
composable("settings") { SettingsScreen(navController) { navController.popBackStack() } }
composable("display_settings") { DisplayScreenSettings(applicationContext) { navController.popBackStack() } }
composable("nav_settings") { NavigationScreenSettings(applicationContext) { navController.popBackStack() } }
}
}
@Composable
fun Settings(navController: NavController, modifier: Modifier = Modifier) {
Box(
modifier = Modifier.fillMaxSize()
) {
FloatingActionButton(
modifier = Modifier.padding(start = 10.dp, top = 40.dp),
onClick = {
navController.navigate("settings")
},
) {
Icon(
painter = painterResource(R.drawable.menu_24px),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(24.dp, 24.dp),
)
}
}
}
@Composable
fun SheetContent(
locationState: Double,
step: StepData?,
nextStep: StepData?,
closeSheet: () -> Unit
locationState: Double, step: StepData?, nextStep: StepData?, closeSheet: () -> Unit
) {
if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() }
} else {
NavigationSheet(
routeModel, step!!, nextStep!!,
applicationContext,
routeModel,
step!!,
nextStep!!,
{ stopNavigation { closeSheet() } },
{ simulateNavigation() }
)
{ simulateNavigation() })
}
// For recomposition!
Text("$locationState", fontSize = 12.sp)
}
fun updateLocation(location: Location?) {
if (location != null
&& lastLocation.latitude != location.position.latitude
&& lastLocation.longitude != location.position.longitude
) {
if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) {
val currentLocation = location(location.position.longitude, location.position.latitude)
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
with(routeModel) {
if (isNavigating()) {
updateLocation(currentLocation, navigationViewModel)
stepData.value = currentStep()
if (route.currentStep + 1 <= legs.steps.size) {
nextStepData.value = nextStep()
}
if (routeState.maneuverType == 39
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
// stopNavigation()
routeState = routeState.copy(arrived = true)
nextStepData.value = nextStep()
if (navState.maneuverType in 39..42 && routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) {
// stopNavigation()
navState.copy(arrived = true)
routeData.value = ""
}
}
}
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
val zoom = calculateZoom(location.speed)
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = zoom,
target = location.position,
bearing = bearing
zoom = zoom, target = location.position, bearing = bearing
),
)
lastLocation = currentLocation
if (!loadRecentPlaces) {
navigationViewModel.loadRecentPlaces(applicationContext, lastLocation)
navigationViewModel.loadRecentPlaces(applicationContext, lastLocation, 0F)
loadRecentPlaces = true
}
}
}
fun stopNavigation(closeSheet: () -> Unit) {
val latitude = routeModel.curRoute.waypoints[0][1]
val longitude = routeModel.curRoute.waypoints[0][0]
closeSheet()
routeModel.stopNavigation()
if (useMock) {
mock.setMockLocation(latitude, longitude)
}
routeData.value = ""
stepData.value = StepData("", 0.0, 0, 0, 0, 0.0)
}
@@ -276,14 +346,10 @@ class MainActivity : ComponentActivity() {
private fun checkMockLocationEnabled() {
try {
// Check if mock location is enabled for this app
val appOpsManager =
getSystemService(APP_OPS_SERVICE) as AppOpsManager
val mode =
appOpsManager.checkOp(
AppOpsManager.OPSTR_MOCK_LOCATION,
Process.myUid(),
packageName
)
val appOpsManager = getSystemService(APP_OPS_SERVICE) as AppOpsManager
val mode = appOpsManager.checkOp(
AppOpsManager.OPSTR_MOCK_LOCATION, Process.myUid(), packageName
)
if (mode != AppOpsManager.MODE_ALLOWED) {
Toast.makeText(
@@ -297,13 +363,14 @@ class MainActivity : ComponentActivity() {
}
}
fun simulate() {
fun simulate() {
CoroutineScope(Dispatchers.IO).launch {
for ((index, step) in routeModel.legs.steps.withIndex()) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
if (routeModel.isNavigating()) {
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
if (routeModel.isNavigating()) {
val deviation = 0.0
if (index in 0..routeModel.curRoute.waypoints.size) {
mock.setMockLocation(waypoint[1], waypoint[0])
delay(800L) //
Thread.sleep(500)
}
}
}
@@ -311,15 +378,67 @@ class MainActivity : ComponentActivity() {
}
fun test() {
for ((index, step) in routeModel.legs.steps.withIndex()) {
println("${step.maneuver.waypoints.size}")
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
//if (index in 3..3) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
routeModel.updateLocation(location(waypoint[0], waypoint[1]), navigationViewModel)
routeModel.currentStep()
if (index + 1 <= routeModel.legs.steps.size) {
nextStepData.value = routeModel.nextStep()
routeModel.updateLocation(
location(waypoint[0], waypoint[1]), navigationViewModel
)
val step = routeModel.currentStep()
val nextStep = routeModel.nextStep()
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
}
//}
}
}
fun testSingle() {
testSingleUpdate(48.185976, 11.578463) // Silcherstr. 23-13
testSingleUpdate(48.186712, 11.578574) // Silcherstr. 27-33
testSingleUpdate(48.186899, 11.580480) // Schmalkadenerstr. 24-28
}
fun testSingleUpdate(latitude: Double, longitude: Double) {
if (1 == 1) {
mock.setMockLocation(latitude, longitude)
} else {
routeModel.updateLocation(
location(longitude, latitude), navigationViewModel
)
}
val step = routeModel.currentStep()
val nextStep = routeModel.nextStep()
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
Thread.sleep(1_000)
}
fun gpx(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
val parser = GPXParser()
val input = context.resources.openRawResource(R.raw.vh)
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
parsedGpx?.let {
val tracks = parsedGpx.tracks
tracks.forEach { tr ->
val segments: MutableList<TrackSegment?>? = tr.trackSegments
segments!!.forEach { seg ->
var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p ->
val ext = p.extensions
val speed: Double?
if (ext != null) {
speed = ext.speed
mock.curSpeed = speed.toFloat()
}
val duration = p.time.millis - lastTime.millis
mock.setMockLocation(p.latitude, p.longitude)
if (duration > 0) {
delay(duration / 5)
}
lastTime = p.time
}
}
}
println(routeModel.routeState.maneuverType)
}
}
}

View File

@@ -7,18 +7,16 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.map.DarkMode
import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.data.Constants
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.tomtom.TrafficData
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect
@@ -32,12 +30,15 @@ fun MapView(
applicationContext: Context,
userLocationState: UserLocationState,
step: StepData?,
nextStep: StepData?,
cameraPosition: MutableLiveData<CameraPosition>,
routeData: MutableLiveData<String>,
tilt: Double
tilt: Double,
baseStyle: BaseStyle.Json,
) {
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(applicationContext)
val metrics =
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(applicationContext)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
@@ -55,21 +56,20 @@ fun MapView(
zoom = 15.0,
)
)
val baseStyle = remember {
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
}
DarkMode(applicationContext, baseStyle)
val rememberBaseStyle = rememberBaseStyle( baseStyle)
Column {
NavigationInfo(step)
NavigationInfo(step, nextStep)
Box(contentAlignment = Alignment.Center) {
MapLibre(
applicationContext,
cameraState,
baseStyle,
rememberBaseStyle,
route,
emptyMap(),
ViewStyle.VIEW
)
LocationTrackingEffect(
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(

View File

@@ -18,7 +18,7 @@ import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.round
@Composable
fun NavigationInfo(step: StepData?) {
fun NavigationInfo(step: StepData?, nextStep: StepData?) {
if (step != null && step.instruction.isNotEmpty()) {
Card(modifier = Modifier.padding(top = 60.dp)) {
Column() {
@@ -28,6 +28,10 @@ fun NavigationInfo(step: StepData?) {
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
if (step.currentManeuverType == 46
|| step.currentManeuverType == 45) {
Text(text ="Exit ${step.exitNumber}", fontSize = 20.sp)
}
Column {
if (step.leftStepDistance < 1000) {
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp)
@@ -39,11 +43,13 @@ fun NavigationInfo(step: StepData?) {
}
Text(text = step.instruction, fontSize = 20.sp)
}
Icon(
painter = painterResource(step.icon),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
if (nextStep != null && step.icon != nextStep.icon) {
Icon(
painter = painterResource(nextStep.icon),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
}
}
}
}

View File

@@ -0,0 +1,162 @@
package com.kouros.navigation.ui
import android.content.Context
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.alorma.compose.settings.ui.SettingsCheckbox
import com.alorma.compose.settings.ui.SettingsRadioButton
import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.utils.NavigationUtils
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavigationScreenSettings(context: Context, navigateBack: () -> Unit) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.navigation_settings),
)
},
navigationIcon = {
IconButton(onClick = navigateBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
}
},
)
},
) { padding ->
val scrollState = rememberScrollState()
Column(
modifier =
Modifier
.consumeWindowInsets(padding)
.verticalScroll(scrollState)
.padding(top = padding.calculateTopPadding()),
) {
NavigationSettings(context)
}
}
}
@Composable
private fun NavigationSettings(context: Context) {
Section(title = stringResource(id = R.string.options)) {
val avoidMotorwayState = remember {
mutableStateOf(
NavigationUtils.getBooleanKeyValue(
context,
Constants.AVOID_MOTORWAY
)
)
}
SettingsCheckbox(
state = avoidMotorwayState.value,
title = { Text(text = stringResource(id = R.string.avoid_highways_row_title)) },
onCheckedChange = {
avoidMotorwayState.value = it
NavigationUtils.setBooleanKeyValue(context, it, Constants.AVOID_MOTORWAY)
},
)
val avoidTollwayState = remember {
mutableStateOf(
NavigationUtils.getBooleanKeyValue(
context,
Constants.AVOID_TOLLWAY
)
)
}
SettingsCheckbox(
state = avoidTollwayState.value,
title = { Text(text = stringResource(id = R.string.avoid_tolls_row_title)) },
onCheckedChange = {
avoidTollwayState.value = it
NavigationUtils.setBooleanKeyValue(context, it, Constants.AVOID_TOLLWAY)
},
)
val carLocationState = remember {
mutableStateOf(
NavigationUtils.getBooleanKeyValue(
context,
Constants.CAR_LOCATION
)
)
}
SettingsCheckbox(
state = carLocationState.value,
title = { Text(text = stringResource(id = R.string.use_car_location)) },
onCheckedChange = {
carLocationState.value = it
NavigationUtils.setBooleanKeyValue(context, it, Constants.CAR_LOCATION)
},
)
}
Section(title = stringResource(id = R.string.routing_engine)) {
val state = remember {
mutableIntStateOf(
NavigationUtils.getIntKeyValue(
context,
ROUTING_ENGINE
)
)
}
RoutingEngineData.engines.forEach { sampleItem ->
SettingsRadioButton(
state = state.intValue == sampleItem.key,
title = { Text(text = sampleItem.title) },
onClick = {
state.intValue = sampleItem.key
NavigationUtils.setIntKeyValue(context, state.intValue, ROUTING_ENGINE)
},
)
}
}
}
internal object RoutingEngineData {
val engines =
listOf(
Item(
key = 0,
title = RouteEngine.VALHALLA.toString(),
),
Item(
key = 1,
title = RouteEngine.OSRM.toString(),
),
Item(
key = 2,
title = RouteEngine.TOMTOM.toString(),
),
)
}

View File

@@ -1,20 +1,21 @@
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.formatDateTime
@@ -23,19 +24,26 @@ import com.kouros.navigation.utils.round
@Composable
fun NavigationSheet(
applicationContext: Context,
routeModel: RouteModel,
step: StepData,
nextStep: StepData,
stopNavigation: () -> Unit,
simulateNavigation: () -> Unit,
) {
val distance = step.leftDistance.round(1)
val distance = (step.leftDistance / 1000).round(1)
if (step.lane.isNotEmpty()) {
routeModel.navState.iconMapper.addLanes( step)
}
Column {
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
Text(formatDateTime(step.arrivalTime), fontSize = 22.sp)
Spacer(Modifier.size(30.dp))
Text("$distance km", fontSize = 22.sp)
}
HorizontalDivider()
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
if (routeModel.isNavigating()) {
@@ -48,6 +56,15 @@ fun NavigationSheet(
modifier = Modifier.size(24.dp, 24.dp),
)
}
Button(onClick = {
simulateNavigation()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_zoom_in_24),
"Stop",
modifier = Modifier.size(24.dp, 24.dp),
)
}
}
Spacer(Modifier.size(30.dp))
if (!routeModel.isNavigating()) {

View File

@@ -57,38 +57,31 @@ fun SearchSheet(
if (search.value != null) {
searchResults.addAll(search.value!!)
}
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
if (searchResults.isNotEmpty()) {
val textFieldState = rememberTextFieldState()
val items = listOf(searchResults)
if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location,
closeSheet = { closeSheet() }
)
}
}
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
if (recentPlaces.value != null) {
val textFieldState = rememberTextFieldState()
val items = listOf(recentPlaces)
if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location,
closeSheet = { closeSheet() }
)
RecentPlaces(recentPlaces.value!!, viewModel, applicationContext, location, closeSheet)
}
}
// if (searchResults.isNotEmpty()) {
val textFieldState = rememberTextFieldState()
val items = listOf(searchResults)
// if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = emptyList<Place>(),
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location,
closeSheet = { closeSheet() }
)
// }
//}
}
@@ -103,7 +96,7 @@ fun Home(
Button(onClick = {
val places = viewModel.loadRecentPlace()
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
viewModel.loadRoute(applicationContext, location, toLocation)
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
closeSheet()
}) {
Icon(
@@ -138,15 +131,7 @@ fun SearchBar(
closeSheet: () -> Unit
) {
var expanded by rememberSaveable { mutableStateOf(true) }
Box(
modifier
.fillMaxSize()
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
@@ -179,7 +164,6 @@ fun SearchBar(
SearchPlaces(searchResults, viewModel, context, location, closeSheet)
}
}
}
}
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
@@ -223,7 +207,7 @@ private fun SearchPlaces(
viewModel.saveRecent(pl)
val toLocation =
location(place.lon.toDouble(), place.lat.toDouble())
viewModel.loadRoute(context, location, toLocation)
viewModel.loadRoute(context, location, toLocation, 0F)
closeSheet()
}
.fillMaxWidth()
@@ -261,7 +245,7 @@ private fun RecentPlaces(
modifier = Modifier
.clickable {
val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation)
viewModel.loadRoute(context, location, toLocation, 0F)
closeSheet()
}
.fillMaxWidth()

View File

@@ -0,0 +1,86 @@
package com.kouros.navigation.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.alorma.compose.settings.ui.SettingsMenuLink
import com.kouros.data.R
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SettingsScreen(navController: NavHostController, navigateBack: () -> Unit) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.settings_action_title),
)
},
navigationIcon = {
IconButton(onClick = navigateBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
}
},
)
},
) { padding ->
val scrollState = rememberScrollState()
Column(
modifier =
Modifier
.consumeWindowInsets(padding)
.verticalScroll(scrollState)
.padding(top = padding.calculateTopPadding()),
) {
SettingsMenuLink(
title = { Text(text = stringResource(R.string.display_settings)) },
modifier = Modifier,
enabled = true,
onClick = { navController.navigate("display_settings")},
icon = {
Icon(
painter = painterResource(R.drawable.ic_place_white_24dp),
contentDescription = stringResource(id = R.string.display_settings),
modifier = Modifier.size(48.dp, 48.dp),
)
}
)
SettingsMenuLink(
title = { Text(text = stringResource(R.string.navigation_settings)) },
modifier = Modifier,
enabled = true,
onClick = { navController.navigate("nav_settings")},
icon = {
Icon(
painter = painterResource(R.drawable.navigation_24px),
contentDescription = stringResource(id = R.string.navigation_settings),
modifier = Modifier.size(48.dp, 48.dp),
)
}
)
}
}
}

View File

@@ -2,10 +2,75 @@ package com.kouros.navigation.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
//val Purple80 = Color(0xFFD0BCFF)
//val PurpleGrey80 = Color(0xFFCCC2DC)
//val Pink80 = Color(0xFFEFB8C8)
//
//val Purple40 = Color(0xFF6650a4)
//val PurpleGrey40 = Color(0xFF625b71)
//val Pink40 = Color(0xFF7D5260)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val md_theme_light_primary = Color(0xFF825500)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
val md_theme_light_secondary = Color(0xFF6F5B40)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
val md_theme_light_tertiary = Color(0xFF51643F)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF1F1B16)
val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF1F1B16)
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
val md_theme_light_outline = Color(0xFF817567)
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
val md_theme_light_inverseSurface = Color(0xFF34302A)
val md_theme_light_inversePrimary = Color(0xFFFFB951)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF825500)
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFFFB951)
val md_theme_dark_onPrimary = Color(0xFF452B00)
val md_theme_dark_primaryContainer = Color(0xFF633F00)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
val md_theme_dark_secondary = Color(0xFFDDC2A1)
val md_theme_dark_onSecondary = Color(0xFF3E2D16)
val md_theme_dark_secondaryContainer = Color(0xFF56442A)
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
val md_theme_dark_onTertiary = Color(0xFF243515)
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF1F1B16)
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
val md_theme_dark_surface = Color(0xFF1F1B16)
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
val md_theme_dark_outline = Color(0xFF9C8F80)
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
val md_theme_dark_inversePrimary = Color(0xFF825500)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
val md_theme_dark_scrim = Color(0xFF000000)
val seed = Color(0xFF825500)

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2022 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
*
* https://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.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
val shapes = Shapes(
extraSmall = RoundedCornerShape(4.dp),
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(16.dp),
large = RoundedCornerShape(24.dp),
extraLarge = RoundedCornerShape(32.dp)
)

View File

@@ -1,57 +1,111 @@
package com.kouros.navigation.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
private val LightColors = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
private val DarkColors = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)
@Composable
fun NavigationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
val context = LocalContext.current
val colors = run {
if (useDarkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colors.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
useDarkTheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = typography,
content = content,
shapes = shapes,
)
}
}

View File

@@ -7,28 +7,35 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
val typography = Typography(
headlineSmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
titleLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
letterSpacing = 0.15.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
bodyMedium = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
labelMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
)

View File

@@ -11,7 +11,6 @@ buildscript {
val objectboxVersion by extra("5.0.1") // For KTS build scripts
dependencies {
// Android Gradle Plugin 8.0 or later supported
classpath(libs.gradle)
classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion")
}

View File

@@ -46,6 +46,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.ui)
implementation(libs.maplibre.compose)
implementation(libs.androidx.app.projected)
//implementation(libs.maplibre.composeMaterial3)
implementation(project(":common:data"))

View File

@@ -32,8 +32,11 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED"/>
<uses-permission android:name="android.car.permission.READ_CAR_DISPLAY_UNITS"/>
<application android:requestLegacyExternalStorage="true">
<application android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true">
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="1" />

View File

@@ -38,9 +38,4 @@ class NavigationCarAppService : CarAppService() {
}
}
public interface LocationCallback {
fun onLocationChanged(location: Location) {
}
}

View File

@@ -5,6 +5,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.location.Location
import android.location.LocationManager
import android.util.Log
@@ -12,6 +13,13 @@ import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.Session
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener
import androidx.car.app.hardware.info.CarHardwareLocation
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.Speed
import androidx.core.location.LocationListenerCompat
import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver
@@ -22,17 +30,18 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.data.Constants.CAR_LOCATION
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getViewModel
class NavigationSession : Session(), NavigationScreen.Listener {
@@ -45,27 +54,33 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lateinit var surfaceRenderer: SurfaceRenderer
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
updateLocation(location!!)
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
if (!useCarLocation) {
updateLocation(location!!)
}
}
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
Log.i(TAG, "In onCreate()")
}
override fun onResume(owner: LifecycleOwner) {
Log.i(TAG, "In onResume()")
}
override fun onPause(owner: LifecycleOwner) {
Log.i(TAG, "In onPause()")
}
override fun onStop(owner: LifecycleOwner) {
Log.i(TAG, "In onStop()")
}
override fun onDestroy(owner: LifecycleOwner) {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.removeCarHardwareLocationListener(carLocationListener)
}
carInfo.removeSpeedListener(carSpeedListener)
Log.i(TAG, "In onDestroy()")
val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
@@ -73,26 +88,67 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
}
lateinit var navigationViewModel : ViewModel
lateinit var navigationViewModel: ViewModel
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
OnCarDataAvailableListener { data ->
if (data.location.status == CarValue.STATUS_SUCCESS) {
val location = data.location.value
if (location != null) {
updateLocation(location)
}
}
}
val carCompassListener: OnCarDataAvailableListener<Compass?> =
OnCarDataAvailableListener { data ->
if (data.orientations.status == CarValue.STATUS_SUCCESS) {
val orientation = data.orientations.value
if (orientation != null) {
surfaceRenderer.carOrientation = orientation[0]
}
}
}
val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
val speed = data.displaySpeedMetersPerSecond.value
surfaceRenderer.updateCarSpeed(speed!!)
}
}
init {
val lifecycle: Lifecycle = lifecycle
lifecycle.addObserver(mLifeCycleObserver)
}
fun onRoutingEngineStateUpdated(routeEngine : Int) {
navigationViewModel = when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
else -> ViewModel(TomTomRepository())
}
}
override fun onCreateScreen(intent: Intent): Screen {
navigationViewModel = getRouteEngine(carContext)
navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
routeModel = RouteCarModel()
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)
navigationScreen = NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
navigationScreen =
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED && !useContacts
== PackageManager.PERMISSION_GRANTED
&& carContext.checkSelfPermission("com.google.android.gms.permission.CAR_SPEED") == PackageManager.PERMISSION_GRANTED
&& !useContacts
|| (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED)
== PackageManager.PERMISSION_GRANTED)
) {
requestLocationUpdates()
} else {
@@ -111,9 +167,29 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
)
}
addSensors()
return navigationScreen
}
fun addSensors() {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL,
carContext.mainExecutor,
carCompassListener)
carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_FASTEST,
carContext.mainExecutor,
carLocationListener
)
}
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
}
override fun onNewIntent(intent: Intent) {
val screenManager = carContext.getCarService(ScreenManager::class.java)
if ((CarContext.ACTION_NAVIGATE == intent.action)) {
@@ -124,8 +200,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
SearchScreen(
carContext,
surfaceRenderer,
location,
navigationViewModel,
navigationViewModel
// TODO: Uri
)
) { obj: Any? ->
@@ -151,16 +226,22 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
}
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
println("Configuration: ${newConfiguration.isNightModeActive}")
super.onCarConfigurationChanged(newConfiguration)
}
@SuppressLint("MissingPermission")
fun requestLocationUpdates() {
val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (location != null) {
navigationViewModel.loadRecentPlace(location = location, surfaceRenderer.carOrientation, carContext)
updateLocation(location)
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 1000,
/* minTimeMs= */ 500,
/* minDistanceM= */ 5f,
mLocationListener
)
@@ -169,17 +250,19 @@ class NavigationSession : Session(), NavigationScreen.Listener {
fun updateLocation(location: Location) {
if (routeModel.isNavigating()) {
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snapedLocation)
if (distance > MAXIMAL_ROUTE_DEVIATION) {
navigationScreen.calculateNewRoute(routeModel.routeState.destination)
return
}
navigationScreen.updateTrip(location)
if (distance < MAXIMAL_SNAP_CORRECTION) {
surfaceRenderer.updateLocation(snapedLocation)
} else {
surfaceRenderer.updateLocation(location)
if (!routeModel.navState.arrived) {
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snapedLocation)
if (distance > MAXIMAL_ROUTE_DEVIATION) {
navigationScreen.calculateNewRoute(routeModel.navState.destination)
return
}
if (distance < MAXIMAL_SNAP_CORRECTION) {
surfaceRenderer.updateLocation(snapedLocation)
} else {
surfaceRenderer.updateLocation(location)
}
}
} else {
surfaceRenderer.updateLocation(location)

View File

@@ -5,8 +5,6 @@ import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.location.Location
import android.os.CountDownTimer
import android.os.Handler
import android.util.Log
import androidx.car.app.AppManager
import androidx.car.app.CarContext
@@ -18,8 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
@@ -27,16 +23,21 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.map.DarkMode
import com.kouros.navigation.car.map.DrawNavigationImages
import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom
@@ -55,10 +56,12 @@ class SurfaceRenderer(
) : DefaultLifecycleObserver {
var lastLocation = location(0.0, 0.0)
var carOrientation = 999F
private val cameraPosition = MutableLiveData(
CameraPosition(
zoom = 15.0,
target = Position(latitude = homeLocation.latitude, longitude = homeLocation.longitude)
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
)
)
private var visibleArea = MutableLiveData(
@@ -69,14 +72,21 @@ class SurfaceRenderer(
var height = 0
var lastBearing = 0.0
val routeData = MutableLiveData("")
val trafficData = MutableLiveData(emptyMap<String, String>())
val speedCamerasData = MutableLiveData("")
val speed = MutableLiveData(0F)
lateinit var centerLocation: Location
val maxSpeed = MutableLiveData(0)
var viewStyle = ViewStyle.VIEW
lateinit var centerLocation: Location
var previewDistance = 0.0
lateinit var mapView: ComposeView
var tilt = 55.0
var countDownTimerActive = false
val style: MutableLiveData<BaseStyle> by lazy {
MutableLiveData()
}
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
lateinit var lifecycleOwner: CustomLifecycleOwner
@@ -159,27 +169,35 @@ class SurfaceRenderer(
init {
lifecycle.addObserver(this)
speed.value = 0F
}
fun onConnectionStateUpdated(connectionState: Int) {
when (connectionState) {
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext)
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto"
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext) // Automotive OS
}
}
fun onBaseStyleStateUpdated(style: BaseStyle) {
}
@Composable
fun MapView() {
val darkMode = getIntKeyValue(carContext, Constants.DARK_MODE_SETTINGS)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val traffic: Map<String, String> ? by trafficData.observeAsState()
val speedCameras: String? by speedCamerasData.observeAsState()
val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle = remember {
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
}
DarkMode(carContext, baseStyle)
MapLibre(carContext, cameraState, baseStyle, route, viewStyle, speedCameras)
val rememberBaseStyle = rememberBaseStyle(baseStyle)
MapLibre(carContext, cameraState, rememberBaseStyle, route, traffic, viewStyle, speedCameras)
ShowPosition(cameraState, position, paddingValues)
}
@@ -192,11 +210,12 @@ class SurfaceRenderer(
val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
val currentSpeed: Float? by speed.observeAsState()
if (viewStyle == ViewStyle.VIEW) {
val maxSpeed: Int? by maxSpeed.observeAsState()
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
DrawNavigationImages(
paddingValues,
currentSpeed,
routeModel.routeState.maxSpeed,
maxSpeed!!,
width,
height
)
@@ -217,6 +236,7 @@ class SurfaceRenderer(
override fun onCreate(owner: LifecycleOwner) {
CarConnection(carContext).type.observe(owner, ::onConnectionStateUpdated)
style.observe(owner, :: onBaseStyleStateUpdated)
Log.i(TAG, "SurfaceRenderer created")
carContext.getCarService(AppManager::class.java)
.setSurfaceCallback(mSurfaceCallback)
@@ -245,11 +265,14 @@ class SurfaceRenderer(
fun updateLocation(location: Location) {
synchronized(this) {
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
val bearing = bearing(
val bearing = if (carOrientation == 999F)
bearing(
lastLocation,
location,
cameraPosition.value!!.bearing
)
) else {
carOrientation.toDouble()
}
val zoom = if (viewStyle == ViewStyle.VIEW) {
calculateZoom(location.speed.toDouble())
} else {
@@ -262,57 +285,39 @@ class SurfaceRenderer(
)
lastBearing = cameraPosition.value!!.bearing
lastLocation = location
speed.value = location.speed
if (!countDownTimerActive) {
countDownTimerActive = true
val mainThreadHandler = Handler(carContext.mainLooper)
val lastLocationTimer = lastLocation
checkUpdate(mainThreadHandler, lastLocationTimer)
}
}
}
}
private fun checkUpdate(
mainThreadHandler: Handler,
lastLocationTimer: Location
) {
mainThreadHandler.post {
object : CountDownTimer(3000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
countDownTimerActive = false
if (lastLocation.time - lastLocationTimer.time < 1500) {
speed.postValue(0F)
}
}
}.start()
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
synchronized(this) {
cameraPosition.postValue(
cameraPosition.value!!.copy(
bearing = bearing,
zoom = zoom,
tilt = tilt,
padding = getPaddingValues(height, viewStyle),
target = target
)
)
}
}
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
cameraPosition.postValue(
cameraPosition.value!!.copy(
bearing = bearing,
zoom = zoom,
tilt = tilt,
padding = getPaddingValues(height, viewStyle),
target = target
)
)
fun setRouteData() {
routeData.value = routeModel.curRoute.routeGeoJson
viewStyle = ViewStyle.VIEW
}
fun setRouteData() {
routeData.value = routeModel.route.routeGeoJson
viewStyle = ViewStyle.VIEW
fun setTrafficData(traffic: Map<String, String> ) {
trafficData.value = traffic as MutableMap<String, String>?
}
fun setPreviewRouteData(routeModel: RouteModel) {
viewStyle = ViewStyle.PREVIEW
with(routeModel) {
routeData.value = route.routeGeoJson
centerLocation = route.centerLocation
previewDistance = route.summary!!.distance
routeData.value = curRoute.routeGeoJson
centerLocation = curRoute.centerLocation
previewDistance = curRoute.summary.distance
}
updateCameraPosition(
0.0,
@@ -322,13 +327,26 @@ class SurfaceRenderer(
}
fun setCategories(location: Location, route: String) {
viewStyle = ViewStyle.AMENITY_VIEW
routeData.value = route
updateCameraPosition(
0.0,
12.0,
target = Position(location.longitude, location.latitude)
)
synchronized(this) {
viewStyle = ViewStyle.AMENITY_VIEW
routeData.value = route
updateCameraPosition(
0.0,
14.0,
target = Position(location.longitude, location.latitude)
)
}
}
fun updateCarLocation(location: Location) {
val routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
if (routingEngine == RouteEngine.OSRM.ordinal) {
updateLocation(location)
}
}
fun updateCarSpeed(newSpeed: Float) {
speed.value = newSpeed
}
fun setCategoryLocation(location: Location, category: String) {

View File

@@ -1,9 +1,8 @@
package com.kouros.navigation.car.map
import android.location.Location
import android.content.Context
import android.location.Location
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@@ -11,7 +10,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -33,17 +33,18 @@ import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.ast.Expression
import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.expressions.dsl.exponential
import org.maplibre.compose.expressions.dsl.image
import org.maplibre.compose.expressions.dsl.interpolate
import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.expressions.value.ColorValue
import org.maplibre.compose.layers.Anchor
import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer
@@ -87,8 +88,9 @@ fun cameraState(
fun MapLibre(
context: Context,
cameraState: CameraState,
baseStyle: MutableState<BaseStyle.Uri>,
baseStyle: BaseStyle.Json,
route: String?,
traffic: Map<String, String>?,
viewStyle: ViewStyle,
speedCameras: String? = ""
) {
@@ -98,7 +100,7 @@ fun MapLibre(
OrnamentOptions(isScaleBarEnabled = false)
),
cameraState = cameraState,
baseStyle = baseStyle.value
baseStyle = baseStyle
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
@@ -107,7 +109,8 @@ fun MapLibre(
if (viewStyle == ViewStyle.AMENITY_VIEW) {
AmenityLayer(route)
} else {
RouteLayer(route)
RouteLayer(route, traffic!!)
//RouteLayerPoint(route )
}
SpeedCameraLayer(speedCameras)
}
@@ -115,8 +118,9 @@ fun MapLibre(
//Puck(cameraState, lastLocation)
}
}
@Composable
fun RouteLayer(routeData: String?) {
fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
if (routeData != null && routeData.isNotEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
LineLayer(
@@ -148,23 +152,105 @@ fun RouteLayer(routeData: String?) {
),
)
}
trafficData.forEach {
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
LineLayer(
id = "traffic-${it.key}-casing",
source = traffic,
color = const(Color.White),
width =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4.dp),
6 to const(0.6.dp),
7 to const(1.8.dp),
20 to const(20.dp),
),
)
LineLayer(
id = "traffic-${it.key}",
source = traffic,
color = trafficColor(it.key),
width =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4.dp),
6 to const(0.5.dp),
7 to const(1.6.dp),
20 to const(18.dp),
),
)
}
}
@Composable
fun RouteLayerPoint(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
val img = image(painterResource(R.drawable.ic_favorite_filled_white_24dp), drawAsSdf = true)
SymbolLayer(
id = "point-layer",
source = routes,
iconOpacity = const(2.0f),
iconColor = const(Color.Red),
iconImage = img,
iconSize =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4f),
6 to const(0.6f),
7 to const(0.8f),
20 to const(1.0f),
),
)
}
}
fun trafficColor(key: String): Expression<ColorValue> {
when (key) {
"queuing" -> return const(Color(0xFFD24417))
"stationary" -> return const(Color(0xFFFF0000))
"heavy" -> return const(Color(0xFF6B0404))
"slow" -> return const(Color(0xFFC41F1F))
"roadworks" -> return const(Color(0xFF7A631A))
}
return const(Color.Blue)
}
@Composable
fun AmenityLayer(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) {
val color = if (routeData.contains(Constants.PHARMACY)) {
const(Color.Red)
} else {
const(Color.Green)
var color = const(Color.Red)
var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
if (routeData.contains(Constants.CHARGING_STATION)) {
color = const(Color.Green)
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
} else if (routeData.contains(Constants.FUEL_STATION)) {
color = const(Color.Black)
img = image(painterResource(R.drawable.local_gas_station_48px), drawAsSdf = true)
}
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
SymbolLayer(
id = "amenity-layer",
source = routes,
iconImage = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true),
iconImage = img,
iconColor = color,
iconSize = const(3.0f),
iconOpacity = const(2.0f),
iconSize =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.7f),
6 to const(1.0f),
7 to const(2.0f),
20 to const(4f),
),
)
}
}
@@ -172,25 +258,26 @@ fun AmenityLayer(routeData: String?) {
@Composable
fun SpeedCameraLayer(speedCameras: String?) {
if (speedCameras != null && speedCameras.isNotEmpty()) {
val color = const(Color.DarkGray)
val color = const(Color.Red)
val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras))
SymbolLayer(
id = "speed-camera-layer",
source = cameraSource,
iconImage = image(painterResource(R.drawable.speed_camera_48px), drawAsSdf = true),
iconImage = image(painterResource(R.drawable.speed_camera_24px), drawAsSdf = true),
iconColor = color,
iconSize =
interpolate(
type = exponential(1.2f),
input = zoom(),
5 to const(0.4f),
6 to const(0.7f),
7 to const(1.75f),
20 to const(3f),
5 to const(0.7f),
6 to const(1.0f),
7 to const(2.0f),
20 to const(4f),
),
)
}
}
@Composable
fun BuildingLayer(tiles: Source) {
Anchor.Replace("building-3d") {
@@ -218,6 +305,7 @@ fun DrawNavigationImages(
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
MaxSpeed(width, height, maxSpeed)
}
//DebugInfo(width, height, routeModel)
}
@Composable
@@ -251,7 +339,7 @@ private fun CurrentSpeed(
curSpeed: Float,
maxSpeed: Int
) {
val radius = 32
val radius = 34
Box(
modifier = Modifier
.padding(
@@ -317,7 +405,7 @@ private fun MaxSpeed(
height: Int,
maxSpeed: Int,
) {
val radius = 20
val radius = 24
Box(
modifier = Modifier
.padding(
@@ -367,25 +455,59 @@ private fun MaxSpeed(
}
@Composable
fun DarkMode(context: Context, baseStyle: MutableState<BaseStyle.Uri>) {
val darkMode = getIntKeyValue(context, Constants.DARK_MODE_SETTINGS)
if (darkMode == 0) {
baseStyle.value = BaseStyle.Uri(Constants.STYLE)
fun DebugInfo(
width: Int,
height: Int,
routeModel: RouteModel,
) {
Box(
modifier = Modifier
.padding(
start = 20.dp,
top = 0.dp
),
contentAlignment = Alignment.CenterStart
) {
val textMeasurerLocation = rememberTextMeasurer()
val location = routeModel.navState.currentLocation.latitude.toString()
val styleSpeed = TextStyle(
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
color = Color.Black,
)
val textLayoutLocation = remember(location) {
textMeasurerLocation.measure(location, styleSpeed)
}
Canvas(modifier = Modifier.fillMaxSize()) {
drawText(
textMeasurer = textMeasurerLocation,
text = location,
style = styleSpeed,
topLeft = Offset(
x = center.x - textLayoutLocation.size.width / 2,
y = center.y - textLayoutLocation.size.height / 2,
)
)
}
}
if (darkMode == 1) {
baseStyle.value = BaseStyle.Uri(Constants.STYLE_DARK)
}
if (darkMode == 2) {
baseStyle.value =
(if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
Constants.STYLE
))
}
@Composable
fun rememberBaseStyle(baseStyle: BaseStyle.Json): BaseStyle.Json {
val rememberBaseStyle by remember() {
mutableStateOf(baseStyle)
}
return rememberBaseStyle
}
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) {
ViewStyle.VIEW -> PaddingValues(start = 50.dp, top = distanceFromTop(height).dp)
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(
start = 50.dp,
top = distanceFromTop(height).dp
)
ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp)
else -> PaddingValues(start = 250.dp, bottom = 0.dp)
}

View File

@@ -15,6 +15,7 @@
*/
package com.kouros.navigation.car.navigation
import android.location.Location
import android.text.SpannableString
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
@@ -29,12 +30,20 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.DateTimeWithZone
import androidx.car.app.model.Distance
import androidx.car.app.navigation.model.Lane
import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.location
import java.util.Collections
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
@@ -46,46 +55,61 @@ class RouteCarModel() : RouteModel() {
val stepData = currentStep()
val currentStepCueWithImage: SpannableString =
createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
}
val step =
Step.Builder(currentStepCueWithImage)
.setManeuver(
Maneuver.Builder(stepData.maneuverType)
.setIcon(createCarIcon(carContext, stepData.icon))
.build()
maneuver.build()
)
.setRoad(routeState.destination.street!!)
.build()
return step
if (navState.destination.street != null) {
step.setRoad(navState.destination.street!!)
}
if (stepData.lane.isNotEmpty()) {
addLanes(carContext, step, stepData)
}
return step.build()
}
/** Returns the next [Step] with information such as the cue text and images. */
fun nextStep(carContext: CarContext): Step? {
fun nextStep(carContext: CarContext): Step {
val stepData = nextStep()
val currentStepCueWithImage: SpannableString =
createString(stepData.instruction)
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
.setIcon(createCarIcon(carContext, stepData.icon))
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
) {
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
}
val step =
Step.Builder(currentStepCueWithImage)
.setManeuver(
Maneuver.Builder(stepData.maneuverType)
.setIcon(createCarIcon(carContext, stepData.icon))
.build()
maneuver.build()
)
.build()
return step
}
fun travelEstimate(carContext: CarContext): TravelEstimate {
val timeLeft = travelLeftTime()
val timeLeft = routeCalculator.travelLeftTime()
val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
val leftDistance = travelLeftDistance()
val leftDistance = routeCalculator.travelLeftDistance() / 1000
val displayUnit = if (leftDistance > 1.0) {
Distance.UNIT_KILOMETERS
} else {
Distance.UNIT_METERS
}
val arivalTime = DateTimeWithZone.create(
arrivalTime(),
val arrivalTime = DateTimeWithZone.create(
routeCalculator.arrivalTime(),
TimeZone.getTimeZone("Europe/Berlin")
)
val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination.
@@ -93,23 +117,52 @@ class RouteCarModel() : RouteModel() {
leftDistance,
displayUnit
), // Arrival time at the destination with the destination time zone.
arivalTime
arrivalTime
)
.setRemainingTimeSeconds(
TimeUnit.MILLISECONDS.toSeconds(
timeToDestinationMillis
)
)
.setRemainingTimeColor(CarColor.YELLOW)
.setRemainingDistanceColor(CarColor.RED)
.setRemainingTimeColor(CarColor.GREEN)
.setRemainingDistanceColor(CarColor.BLUE)
if (routeState.travelMessage.isNotEmpty()) {
if (navState.travelMessage.isNotEmpty()) {
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
travelBuilder.setTripText(CarText.create(routeState.travelMessage))
travelBuilder.setTripText(CarText.create(navState.travelMessage))
}
return travelBuilder.build()
}
fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) {
var laneImageAdded = false
stepData.lane.forEach {
if (it.indications.isNotEmpty() && it.valid) {
Collections.sort<String>(it.indications)
var direction = ""
it.indications.forEach { it2 ->
direction = if (direction.isEmpty()) {
it2.trim()
} else {
"${direction}_${it2.trim()}"
}
}
val laneDirection = navState.iconMapper.addLanes(direction, stepData)
if (laneDirection != LaneDirection.SHAPE_UNKNOWN) {
if (!laneImageAdded) {
step.setLanesImage(createCarIcon(navState.iconMapper.createLaneIcon(carContext, stepData)))
laneImageAdded = true
}
val laneType =
Lane.Builder()
.addDirection(LaneDirection.create(laneDirection, false))
.build()
step.addLane(laneType)
}
}
}
}
fun createString(
text: String
): SpannableString {
@@ -125,23 +178,35 @@ class RouteCarModel() : RouteModel() {
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 createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build()
}
fun createAlert(carContext: CarContext, distance: Double, maxSpeed: String?): Alert {
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) {
carContext.getCarService<AppManager?>(AppManager::class.java)
.showAlert(
createAlert(
carContext,
maxSpeed,
createCarIcon(carContext, R.drawable.speed_camera_24px)
)
)
}
fun createAlert(
carContext: CarContext,
maxSpeed: String?,
icon: CarIcon
): 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.exit_action_title, R.string.exit_action_title,
FLAG_DEFAULT
)
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */10000)
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
.setSubtitle(subtitle)
.setIcon(icon)
.addAction(dismissAction).setCallback(object : AlertCallback {

View File

@@ -14,6 +14,7 @@ import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
@@ -23,8 +24,7 @@ import com.kouros.navigation.model.ViewModel
class CategoriesScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val location: Location,
private val viewModel: ViewModel
private val viewModel: ViewModel,
) : Screen(carContext) {
var categories: List<Category> = listOf(
@@ -47,7 +47,6 @@ class CategoriesScreen(
CategoryScreen(
carContext,
surfaceRenderer,
location,
it.id,
viewModel
)

View File

@@ -19,8 +19,9 @@ import androidx.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils.createPointCollection
@@ -31,9 +32,8 @@ import kotlin.math.min
class CategoryScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
location: Location,
private val category: String,
private val viewModel: ViewModel
private val viewModel: ViewModel,
) : Screen(carContext) {
var elements = listOf<Elements>()
@@ -44,10 +44,10 @@ class CategoryScreen(
val loc = location(0.0, 0.0)
elements.forEach {
if (loc.latitude == 0.0) {
loc.longitude = it.lon!!
loc.latitude = it.lat!!
loc.longitude = it.lon
loc.latitude = it.lat
}
coordinates.add(listOf(it.lon!!, it.lat!!))
coordinates.add(listOf(it.lon, it.lat))
}
if (elements.isNotEmpty()) {
val route = createPointCollection(coordinates, category)
@@ -58,7 +58,7 @@ class CategoryScreen(
init {
viewModel.elements.observe(this, observer)
viewModel.getAmenities(category, location)
viewModel.getAmenities(category, surfaceRenderer.lastLocation)
}
@@ -111,7 +111,7 @@ class CategoryScreen(
}
val row = Row.Builder()
.setOnClickListener {
val location = location(it.lon!!, it.lat!!)
val location = location(it.lon, it.lat)
surfaceRenderer.setCategoryLocation(location, category)
}
.setTitle(name)
@@ -126,6 +126,28 @@ class CategoryScreen(
} else {
row.addText(carText("${it.tags.openingHours}"))
}
val navigationMessage = NavigationMessage(carContext)
row.addAction(
Action.Builder()
.setOnClickListener {
viewModel.loadRoute(
carContext,
currentLocation = surfaceRenderer.lastLocation,
location(it.lon!!, it.lat!!),
surfaceRenderer.carOrientation
)
setResult(
Place(
name = name,
category = Constants.CHARGING_STATION,
latitude = it.lat!!,
longitude = it.lon!!
)
)
finish()
}
.setIcon(navigationMessage.createCarIcon(R.drawable.navigation_48px))
.build())
return row.build()
}

View File

@@ -11,7 +11,6 @@ import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.Distance
import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate
@@ -29,14 +28,14 @@ import com.kouros.navigation.car.ViewStyle
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.NavigationRepository
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.utils.GeoUtils
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.location
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
class NavigationScreen(
@@ -58,6 +57,7 @@ class NavigationScreen(
var recentPlace = Place()
var navigationType = NavigationType.VIEW
var lastTrafficDate = LocalDateTime.of(1960, 6, 21, 0, 0)
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
navigationType = NavigationType.NAVIGATION
@@ -74,6 +74,10 @@ class NavigationScreen(
invalidate()
}
}
val trafficObserver = Observer<Map<String, String> > { traffic ->
surfaceRenderer.setTrafficData(traffic)
invalidate()
}
val placeObserver = Observer<SearchResult> { searchResult ->
val place = Place(
@@ -93,22 +97,19 @@ class NavigationScreen(
var speedCameras = listOf<Elements>()
val speedObserver = Observer<List<Elements>> { cameras ->
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
val loc = location(0.0, 0.0)
cameras.forEach {
val loc =
location(longitude = it.lon!!, latitude = it.lat!!)
coordinates.add(listOf(it.lon!!, it.lat!!))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCamerasData.value =speedData
surfaceRenderer.speedCamerasData.value = speedData
}
init {
viewModel.route.observe(this, observer)
viewModel.traffic.observe(this, trafficObserver);
viewModel.recentPlace.observe(this, recentObserver)
viewModel.loadRecentPlace(location = surfaceRenderer.lastLocation)
viewModel.placeLocation.observe(this, placeObserver)
viewModel.speedCameras.observe(this, speedObserver)
}
@@ -149,11 +150,11 @@ class NavigationScreen(
}
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.routeState.arrived) {
if (routeModel.navState.arrived) {
val timer = object : CountDownTimer(8000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
routeModel.routeState = routeModel.routeState.copy(arrived = false)
routeModel.navState = routeModel.navState.copy(arrived = false)
navigationType = NavigationType.VIEW
invalidate()
}
@@ -172,8 +173,8 @@ class NavigationScreen(
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = ""
if (routeModel.routeState.destination.street != null) {
street = routeModel.routeState.destination.street!!
if (routeModel.navState.destination.street != null) {
street = routeModel.navState.destination.street!!
}
return NavigationTemplate.Builder()
.setNavigationInfo(
@@ -232,7 +233,7 @@ class NavigationScreen(
}
fun getRoutingInfo(): RoutingInfo {
var currentDistance = routeModel.leftStepDistance()
var currentDistance = routeModel.routeCalculator.leftStepDistance()
val displayUnit = if (currentDistance > 1000.0) {
currentDistance /= 1000.0
Distance.UNIT_KILOMETERS
@@ -240,22 +241,13 @@ class NavigationScreen(
Distance.UNIT_METERS
}
val nextStep = routeModel.nextStep(carContext = carContext)
if (nextStep != null) {
return RoutingInfo.Builder()
.setCurrentStep(
routeModel.currentStep(carContext = carContext),
Distance.create(currentDistance, displayUnit)
)
.setNextStep(nextStep)
.build()
} else {
return RoutingInfo.Builder()
.setCurrentStep(
routeModel.currentStep(carContext = carContext),
Distance.create(currentDistance, displayUnit)
)
.build()
}
return RoutingInfo.Builder()
.setCurrentStep(
routeModel.currentStep(carContext = carContext),
Distance.create(currentDistance, displayUnit)
)
.setNextStep(nextStep)
.build()
}
private fun createActionStripBuilder(): ActionStrip.Builder {
@@ -314,8 +306,13 @@ class NavigationScreen(
)
.setOnClickListener {
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo)
routeModel.routeState = routeModel.routeState.copy(destination = recentPlace)
viewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
navigateTo,
surfaceRenderer.carOrientation
)
routeModel.navState = routeModel.navState.copy(destination = recentPlace)
}
.build()
}
@@ -352,7 +349,7 @@ class NavigationScreen(
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext))
screenManager.push(SettingsScreen(carContext, viewModel))
}
.build()
}
@@ -369,6 +366,7 @@ class NavigationScreen(
.build()
).setOnClickListener {
surfaceRenderer.handleScale(1)
invalidate()
}
.build()
}
@@ -385,6 +383,7 @@ class NavigationScreen(
.build()
).setOnClickListener {
surfaceRenderer.handleScale(-1)
invalidate()
}
.build()
}
@@ -401,6 +400,7 @@ class NavigationScreen(
.build()
).setOnClickListener {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
}
.build()
}
@@ -408,7 +408,11 @@ class NavigationScreen(
private fun startSearchScreen() {
screenManager
.pushForResult(
SearchScreen(carContext, surfaceRenderer, surfaceRenderer.lastLocation, viewModel)
SearchScreen(
carContext,
surfaceRenderer,
viewModel
)
) { obj: Any? ->
if (obj != null) {
val place = obj as Place
@@ -430,8 +434,13 @@ class NavigationScreen(
val location = location(place.longitude, place.latitude)
viewModel.saveRecent(place)
currentNavigationLocation = location
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
routeModel.routeState = routeModel.routeState.copy(destination = place)
viewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
routeModel.navState = routeModel.navState.copy(destination = place)
invalidate()
}
@@ -449,7 +458,7 @@ class NavigationScreen(
invalidate()
val mainThreadHandler = Handler(carContext.mainLooper)
mainThreadHandler.post {
object : CountDownTimer(3000, 1000) {
object : CountDownTimer(2000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
navigationType = NavigationType.NAVIGATION
@@ -461,18 +470,32 @@ class NavigationScreen(
fun reRoute(destination: Place) {
val dest = location(destination.longitude, destination.latitude)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, dest)
viewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
dest,
surfaceRenderer.carOrientation
)
}
fun updateTrip(location: Location) {
val current = LocalDateTime.now(ZoneOffset.UTC)
val duration = java.time.Duration.between(current, lastTrafficDate)
if (duration.abs().seconds > 360) {
lastTrafficDate = current
viewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
updateSpeedCamera(location)
with(routeModel) {
updateLocation(location, viewModel)
if (routeState.maneuverType == Maneuver.TYPE_DESTINATION
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
routeState = routeState.copy(arrived = true)
navState = navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()
@@ -496,20 +519,27 @@ class NavigationScreen(
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon!!, latitude = it.lat!!)
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()
val bearingSpeedCamera = location.bearingTo(location(camera.lon!!, camera.lat!!))
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
if (camera.distance < 80
&& (bearingSpeedCamera.absoluteValue - bearingRoute.absoluteValue).absoluteValue < 15.0
) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
val bearingSpeedCamera = if (camera.tags.direction != null) {
try {
camera.tags.direction!!.toFloat()
} catch ( e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
}

View File

@@ -12,20 +12,29 @@ import androidx.car.app.model.Toggle
import com.kouros.data.R
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY
import com.kouros.navigation.data.Constants.CAR_LOCATION
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
class NavigationSettings(private val carContext: CarContext) : Screen(carContext) {
class NavigationSettings(private val carContext: CarContext, private var viewModel: ViewModel) :
Screen(carContext) {
private var motorWayToggleState = false
private var tollWayToggleState = false
private var carLocationToggleState = false
init {
motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
carLocationToggleState = getBooleanKeyValue(carContext, CAR_LOCATION)
}
override fun onGetTemplate(): Template {
@@ -53,6 +62,29 @@ class NavigationSettings(private val carContext: CarContext) : Screen(carContext
}.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
val carLocationToggle: Toggle =
Toggle.Builder { checked: Boolean ->
if (checked) {
setBooleanKeyValue(carContext, true, CAR_LOCATION)
} else {
setBooleanKeyValue(carContext, false, CAR_LOCATION)
}
carLocationToggleState = !carLocationToggleState
}.setChecked(carLocationToggleState).build()
listBuilder.addItem(
buildRowForTemplate(
R.string.use_car_location,
carLocationToggle
)
)
listBuilder.addItem(
buildRowForScreenTemplate(
RoutingSettings(carContext, viewModel),
R.string.routing_engine
)
)
return ListTemplate.Builder()
.setSingleList(listBuilder.build())
.setHeader(
@@ -70,4 +102,12 @@ class NavigationSettings(private val carContext: CarContext) : Screen(carContext
.setToggle(toggle)
.build()
}
private fun buildRowForScreenTemplate(screen: Screen, title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setOnClickListener { screenManager.push(screen) }
.setBrowsable(true)
.build()
}
}

View File

@@ -7,7 +7,6 @@ import android.text.SpannableString
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.CarIcon
import androidx.car.app.model.Distance
@@ -22,21 +21,16 @@ import androidx.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel
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.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel
import kotlin.math.min
class PlaceListScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val location: Location,
private val category: String,
private val viewModel: ViewModel
) : Screen(carContext) {
@@ -68,13 +62,21 @@ class PlaceListScreen(
fun loadPlaces() {
if (category == RECENT) {
viewModel.loadRecentPlaces(carContext, location)
viewModel.loadRecentPlaces(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
if (category == CONTACTS) {
viewModel.loadContacts(carContext)
}
if (category == FAVORITES) {
viewModel.loadFavorites(carContext, location)
viewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
}
@@ -82,9 +84,14 @@ class PlaceListScreen(
val itemListBuilder = ItemList.Builder()
.setNoItemsMessage(carContext.getString(R.string.no_places))
places.forEach {
val street = if (it.street != null) {
it.street
} else {
""
}
val row = Row.Builder()
.setImage(contactIcon(it.avatar, it.category))
.setTitle("${it.street!!} ${it.city}")
.setTitle("$street ${it.city}")
.setOnClickListener {
val place = Place(
0,
@@ -117,7 +124,7 @@ class PlaceListScreen(
setSpan(
DistanceSpan.create(
Distance.create(
it.distance.toDouble(),
(it.distance/1000).toDouble(),
Distance.UNIT_KILOMETERS
)
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE

View File

@@ -26,9 +26,10 @@ class RequestPermissionScreen(
override fun onGetTemplate(): Template {
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION)
permissions.add("com.google.android.gms.permission.CAR_SPEED")
//permissions.add(permission.READ_CONTACTS)
val message = "This app needs access to location in order to show the map around you"
val message = "This app needs access to location and to car speed"
val listener: OnClickListener = ParkedOnlyOnClickListener.create {
carContext.requestPermissions(

View File

@@ -6,11 +6,10 @@ import androidx.annotation.DrawableRes
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.Action.FLAG_DEFAULT
import androidx.car.app.model.Action.FLAG_PRIMARY
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.DurationSpan
@@ -18,19 +17,17 @@ import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.OnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController
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.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location
@@ -61,7 +58,12 @@ class RoutePreviewScreen(
init {
viewModel.previewRoute.observe(this, observer)
val location = location(destination.longitude, destination.latitude)
viewModel.loadPreviewRoute(carContext, surfaceRenderer.lastLocation, location)
viewModel.loadPreviewRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
}
override fun onGetTemplate(): Template {
@@ -74,13 +76,17 @@ class RoutePreviewScreen(
.setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate() }
.build()
val itemListBuilder = ItemList.Builder()
var i = 0
routeModel.route.routes.forEach { it ->
itemListBuilder.addItem(createRow(i++, navigateAction))
}
val header = Header.Builder()
.setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.route_preview))
//.addEndHeaderAction(navigateAction)
.addEndHeaderAction(
favoriteAction()
)
@@ -89,30 +95,40 @@ class RoutePreviewScreen(
)
.build()
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 onFinish() {
//onNavigate()
val message =
if (routeModel.isNavigating() && routeModel.curRoute.waypoints!!.isNotEmpty()) {
createRouteText(0)
} else {
CarText.Builder("Wait")
.build()
}
if (routeModel.route.routes.size == 1) {
val timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
onNavigate()
}
}
timer.start()
}
timer.start()
val content = if (routeModel.route.routes.size > 1) {
ListTemplate.Builder()
.setHeader(header)
.setSingleList(itemListBuilder.build())
.build()
} else {
MessageTemplate.Builder(
message
)
.setHeader(header)
.addAction(navigateAction)
.setLoading(message.toString() == "Wait")
.build()
}
return MapWithContentTemplate.Builder()
.setContentTemplate(messageTemplate)
.setContentTemplate(content)
.setMapController(
MapController.Builder().setMapActionStrip(
getMapActionStrip()
@@ -172,9 +188,13 @@ class RoutePreviewScreen(
)
.build()
private fun createRouteText(): CarText {
val time = routeModel.route.summary!!.duration
val length = BigDecimal(routeModel.route.summary!!.distance).setScale(1, RoundingMode.HALF_EVEN)
private fun createRouteText(index: Int): CarText {
val time = routeModel.route.routes[index].summary.duration
val length =
BigDecimal(routeModel.route.routes[index].summary.distance).setScale(
1,
RoundingMode.HALF_EVEN
)
val firstRoute = SpannableString(" \u00b7 $length km")
firstRoute.setSpan(
DurationSpan.create(time.toLong()), 0, 1, 0
@@ -183,14 +203,27 @@ class RoutePreviewScreen(
.build()
}
private fun createRow(index: Int, action: Action): Row {
val route = createRouteText(index)
val titleText = "$index"
return Row.Builder()
.setTitle(route)
.setOnClickListener(OnClickListener { onRouteSelected(index) })
.addText(titleText)
.addAction(action)
.build()
}
private fun onNavigate() {
setResult(destination)
finish()
}
private fun onRouteSelected(index: Int) {
setResult(destination)
finish()
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
surfaceRenderer.setPreviewRouteData(routeModel)
//setResult(destination)
//finish()
}
fun getMapActionStrip(): ActionStrip {

View File

@@ -0,0 +1,92 @@
package com.kouros.navigation.car.screen
import androidx.car.app.CarContext
import androidx.car.app.CarToast
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.SectionedItemList
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import com.kouros.data.R
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
import com.kouros.navigation.data.Constants.CAR_LOCATION
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
class RoutingSettings(private val carContext: CarContext, private var viewModel: ViewModel) : Screen(carContext) {
private var routingEngine = RouteEngine.OSRM.ordinal
init {
routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
}
override fun onGetTemplate(): Template {
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
.addItem(
buildRowForTemplate(
R.string.valhalla,
)
)
.addItem(
buildRowForTemplate(
R.string.osrm,
)
)
.addItem(
buildRowForTemplate(
R.string.tomtom,
)
)
.setOnSelectedListener { index: Int ->
this.onSelected(index)
}
.setSelectedIndex(routingEngine)
.build()
return templateBuilder
.addSectionedList(SectionedItemList.create(
radioList,
carContext.getString(R.string.routing_engine)
))
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.routing_engine))
.setStartHeaderAction(Action.BACK)
.build()
)
.build()
}
private fun onSelected(index: Int) {
setIntKeyValue(carContext, index, ROUTING_ENGINE)
viewModel.routingEngine.value = index
CarToast.makeText(
carContext,
(carContext
.getString(R.string.routing_engine)
+ ":"
+ " " + index), CarToast.LENGTH_LONG
)
.show()
}
private fun buildRowForTemplate(title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.build()
}
}

View File

@@ -2,7 +2,6 @@ package com.kouros.navigation.car.screen
import android.annotation.SuppressLint
import android.location.Location
import android.net.Uri
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
@@ -17,9 +16,9 @@ import androidx.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.ViewModel
@@ -28,15 +27,14 @@ import com.kouros.navigation.model.ViewModel
class SearchScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private var location: Location,
private val viewModel: ViewModel
private val viewModel: ViewModel,
) : Screen(carContext) {
var isSearchComplete: Boolean = false
var categories: List<Category> = listOf(
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))
)
@@ -73,7 +71,6 @@ class SearchScreen(
CategoriesScreen(
carContext,
surfaceRenderer,
location,
viewModel
)
) { obj: Any? ->
@@ -89,7 +86,6 @@ class SearchScreen(
PlaceListScreen(
carContext,
surfaceRenderer,
location,
it.id,
viewModel
)
@@ -119,7 +115,7 @@ class SearchScreen(
object : SearchCallback {
override fun onSearchSubmitted(searchTerm: String) {
isSearchComplete = true
viewModel.searchPlaces(searchTerm, location)
viewModel.searchPlaces(searchTerm, surfaceRenderer.lastLocation)
}
})
.setHeaderAction(Action.BACK)

View File

@@ -24,10 +24,12 @@ 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.model.ViewModel
/** A screen demonstrating selectable lists. */
class SettingsScreen(
carContext: CarContext,
private var viewModel: ViewModel,
) : Screen(carContext) {
override fun onGetTemplate(): Template {
@@ -40,7 +42,7 @@ class SettingsScreen(
)
listBuilder.addItem(
buildRowForTemplate(
NavigationSettings(carContext),
NavigationSettings(carContext, viewModel),
R.string.navigation_settings
)
)

View File

@@ -1,255 +0,0 @@
{
"trip": {
"locations": [
{
"type": "break",
"lat": 48.185749,
"lon": 11.579374,
"side_of_street": "right",
"original_index": 0
},
{
"type": "break",
"lat": 48.116481,
"lon": 11.594322,
"street": "Hohenwaldeckstr. 27",
"side_of_street": "left",
"original_index": 1
}
],
"legs": [
{
"maneuvers": [
{
"type": 2,
"instruction": "Auf Vogelhartstraße Richtung Westen fahren.",
"verbal_succinct_transition_instruction": "Richtung Westen fahren. Dann Rechts auf Silcherstraße abbiegen.",
"verbal_pre_transition_instruction": "Auf Vogelhartstraße Richtung Westen fahren. Dann Rechts auf Silcherstraße abbiegen.",
"verbal_post_transition_instruction": "70 Meter weiter der Route folgen.",
"street_names": [
"Vogelhartstraße"
],
"bearing_after": 273,
"time": 16.965,
"length": 0.07,
"cost": 34.428,
"begin_shape_index": 0,
"end_shape_index": 6,
"verbal_multi_cue": true,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Rechts auf Silcherstraße abbiegen.",
"verbal_transition_alert_instruction": "Rechts auf Silcherstraße abbiegen.",
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
"verbal_pre_transition_instruction": "Rechts auf Silcherstraße abbiegen.",
"verbal_post_transition_instruction": "200 Meter weiter der Route folgen.",
"street_names": [
"Silcherstraße"
],
"bearing_before": 273,
"bearing_after": 5,
"time": 43.25,
"length": 0.156,
"cost": 89.306,
"begin_shape_index": 6,
"end_shape_index": 13,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
"verbal_transition_alert_instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
"verbal_pre_transition_instruction": "Rechts auf Schmalkaldener Straße abbiegen.",
"verbal_post_transition_instruction": "400 Meter weiter der Route folgen.",
"street_names": [
"Schmalkaldener Straße"
],
"bearing_before": 2,
"bearing_after": 93,
"time": 108.947,
"length": 0.43,
"cost": 217.43,
"begin_shape_index": 13,
"end_shape_index": 29,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Rechts auf Ingolstädter Straße/B 13 abbiegen.",
"verbal_transition_alert_instruction": "Rechts auf Ingolstädter Straße abbiegen.",
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
"verbal_pre_transition_instruction": "Rechts auf Ingolstädter Straße, B 13 abbiegen.",
"verbal_post_transition_instruction": "einen Kilometer weiter der Route folgen.",
"street_names": [
"B 13"
],
"begin_street_names": [
"Ingolstädter Straße",
"B 13"
],
"bearing_before": 88,
"bearing_after": 178,
"time": 147.528,
"length": 1.064,
"cost": 230.646,
"begin_shape_index": 29,
"end_shape_index": 65,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 19,
"instruction": "Auf die Auffahrt nach links abbiegen.",
"verbal_transition_alert_instruction": "Auf die Auffahrt nach links abbiegen.",
"verbal_pre_transition_instruction": "Auf die Auffahrt nach links abbiegen.",
"street_names": [
"Schenkendorfstraße"
],
"bearing_before": 188,
"bearing_after": 98,
"time": 61.597,
"length": 0.374,
"cost": 117.338,
"begin_shape_index": 65,
"end_shape_index": 84,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 24,
"instruction": "Links halten auf B 2R.",
"verbal_transition_alert_instruction": "Links halten auf B 2R.",
"verbal_pre_transition_instruction": "Links halten auf B 2R.",
"verbal_post_transition_instruction": "6 Kilometer weiter der Route folgen.",
"street_names": [
"B 2R"
],
"bearing_before": 117,
"bearing_after": 118,
"time": 509.658,
"length": 6.37,
"cost": 580.602,
"begin_shape_index": 84,
"end_shape_index": 240,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 20,
"instruction": "An der Ausfahrt rechts abfahren.",
"verbal_transition_alert_instruction": "An der Ausfahrt rechts abfahren.",
"verbal_pre_transition_instruction": "An der Ausfahrt rechts abfahren.",
"verbal_post_transition_instruction": "einen Kilometer weiter der Route folgen.",
"street_names": [
"Ampfingstraße"
],
"bearing_before": 191,
"bearing_after": 206,
"time": 133.661,
"length": 1.031,
"cost": 226.661,
"begin_shape_index": 240,
"end_shape_index": 280,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Rechts auf Anzinger Straße abbiegen.",
"verbal_transition_alert_instruction": "Rechts auf Anzinger Straße abbiegen.",
"verbal_succinct_transition_instruction": "Rechts abbiegen.",
"verbal_pre_transition_instruction": "Rechts auf Anzinger Straße abbiegen.",
"verbal_post_transition_instruction": "1.5 Kilometer weiter der Route folgen.",
"street_names": [
"Anzinger Straße"
],
"bearing_before": 182,
"bearing_after": 277,
"time": 211.637,
"length": 1.444,
"cost": 450.654,
"begin_shape_index": 280,
"end_shape_index": 334,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Links auf Hohenwaldeckstraße abbiegen.",
"verbal_transition_alert_instruction": "Links auf Hohenwaldeckstraße abbiegen.",
"verbal_succinct_transition_instruction": "Links abbiegen.",
"verbal_pre_transition_instruction": "Links auf Hohenwaldeckstraße abbiegen.",
"verbal_post_transition_instruction": "200 Meter weiter der Route folgen.",
"street_names": [
"Hohenwaldeckstraße"
],
"bearing_before": 249,
"bearing_after": 170,
"time": 45.365,
"length": 0.183,
"cost": 84.344,
"begin_shape_index": 334,
"end_shape_index": 342,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 6,
"instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
"verbal_transition_alert_instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
"verbal_pre_transition_instruction": "Hohenwaldeckstr. 27 befindet sich auf der linken Seite.",
"bearing_before": 184,
"time": 0,
"length": 0,
"cost": 0,
"begin_shape_index": 342,
"end_shape_index": 342,
"travel_mode": "drive",
"travel_type": "car"
}
],
"summary": {
"level_changes": [
[220, -1]
],
"has_time_restrictions": false,
"has_toll": false,
"has_highway": false,
"has_ferry": false,
"min_lat": 48.116486,
"min_lon": 11.578422,
"max_lat": 48.186957,
"max_lon": 11.616382,
"time": 1278.611,
"length": 11.123,
"cost": 2031.412
},
"shape": "mk_|zA_}vaUA^MzKKhKMrKMrLEbEeLs@kGa@yV}AmIa@cJ]g@AgQc@TgTn@ak@\\}Y`@u~@NiZRss@Ekc@AcGAwE?iB@yN@mH@_L?sAOiVEiGrISbH[|s@kC`KY~Qw@dk@mBdBErH]bIa@pNk@pAGxgA}ErQw@f`@eB`AAhEOjDO~Kg@bh@cCpTcAtEUlBKtFMbk@cBpt@eDfScAlH]hHY`HTdATbBl@rAd@|Bz@xBr@|F`AzD\\l@mHHsCAeDcAkImBiLs@}Ii@wOh@a]vAu[bB{VjCmXjCuUtDoU~A}JjBmIvDmOvDgOfCiJdB}HvAsG|FwSzGaV`IgWdC_K\\cG~Pii@pUcr@dYaz@lEkMdDsPpMm]|Tqj@tQwc@jQsb@nVwm@vEmL`k@suAxHyPzFaKrBaD|AmBxCeDpC}BvDmCnEyBnDuAzFyAhDWdDW|D?~CPjFj@lHrB|QzI~O~Jb]lS`ZbR~NnIhCdBdDtBb`Azk@fhAdr@vN~I~l@|]vr@vVb]jElZw@xG{@jEw@`KiCfLiFrJ_GnPaNjJaJzJoNxMgUtU}g@d]_w@f_@ev@tO_YhRmYbHwIxG_I|NwN~AyAdTiP~b@iZ~J}GxScQ`JoIfGwGtEwFnCqDbEaG~CcFhHgMrGcNxHmR|FaQnCwIpAeEdCiIje@}}AnQil@pGySjCyIvSor@nJ{ZpHwVbQyk@zIkXlHkTrOcc@bPic@rUyk@vKoVnMaWfWed@rT_`@jQcZhBwCzCsEtCcElC}CzCiDrCiCnH{GzLwJlGwDfH_ElGeDhHwClHyBrG{BdK_CzMsBzJgAbIq@jI?nYGbSTfGDvEDnGRfWx@|i@lEvpBbUdRrBpLfAl_@jDr|Ghm@hu@jGrWrBpd@fAxG[raBgUl[{HhM}DzOeGdXiLpCuAzwAij@lEcAbF_AnEc@|DDzE[dAGrIh@|APfe@lLbc@lLpWbIbdAnYnKlBf]|Tzi@rZbl@|ZtSjJpOhG~HvB`AVvBl@hBf@vBl@`AXrJrCtZjHhRvCrKpAjAN|Gb@hLr@dLp@xYbB`CPlDNxBLlOv@n{@jE~F\\lDP|Ov@lSn@rGNrGPlHAxICnJCvJBhFBrQL~E?dC?zFJ[xIcB|_@}Add@kAvi@i@zg@AtGEd_@f@lq@lB`|@jApd@tA`l@XpFn@lHf@bFnAdMjA`HnDpQfEfSvElPfBdOvCrWjP|xANrA|@bH\\pC\\lCNnA~Hhn@dB|MnAtJrAlK`AzH~@hHvBvPj@pEl@zFx@zH^hD~BlQdEbZhF`_@rAbJ|AjK~AtKzBrNt@nFv@lFtB`OdCfQbMb_AlDnUrDvTvF|ZzIxh@jDm@zHgBhF{@lC[`L]jMr@bNj@~_@bB"
}
],
"summary": {
"has_time_restrictions": false,
"has_toll": false,
"has_highway": false,
"has_ferry": false,
"min_lat": 48.116486,
"min_lon": 11.578422,
"max_lat": 48.186957,
"max_lon": 11.616382,
"time": 1278.611,
"length": 11.123,
"cost": 2031.412
},
"status_message": "Found route between points",
"status": 0,
"units": "kilometers",
"language": "de-DE"
},
"id": "my_work_route"
}

View File

@@ -1,11 +1,6 @@
package com.kouros.navigation.car
import android.location.Location
import android.location.LocationManager
import com.kouros.navigation.data.Constants.home2Location
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import org.junit.Test
@@ -16,23 +11,23 @@ import org.junit.Test
*/
class ViewModelTest {
val repo = NavigationRepository()
val repo = ValhallaRepository()
val viewModel = ViewModel(repo)
val model = RouteModel()
@Test
fun routeViewModelTest() {
val fromLocation = Location(LocationManager.GPS_PROVIDER)
fromLocation.isMock = true
fromLocation.latitude = homeLocation.latitude
fromLocation.longitude = homeLocation.longitude
val toLocation = Location(LocationManager.GPS_PROVIDER)
toLocation.isMock = true
toLocation.latitude = home2Location.latitude
toLocation.longitude = home2Location.longitude
val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
model.startNavigation(route)
// val fromLocation = Location(LocationManager.GPS_PROVIDER)
// fromLocation.isMock = true
// fromLocation.latitude = homeLocation.latitude
// fromLocation.longitude = homeLocation.longitude
// val toLocation = Location(LocationManager.GPS_PROVIDER)
// toLocation.isMock = true
// toLocation.latitude = home2Location.latitude
// toLocation.longitude = home2Location.longitude
//
// val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
//model.startNavigation(route)
}
}

View File

@@ -58,7 +58,7 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.maplibre.compose)
implementation("androidx.compose.material:material-icons-extended:1.7.8")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -19,6 +19,8 @@ package com.kouros.navigation.data
import android.location.Location
import android.location.LocationManager
import android.net.Uri
import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.utils.location
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import kotlinx.serialization.Serializable
@@ -57,13 +59,16 @@ data class StepData (
var leftStepDistance: Double,
var maneuverType: Int,
var currentManeuverType: Int,
var icon: Int,
var arrivalTime : Long,
var leftDistance: Double
var leftDistance: Double,
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
var exitNumber: Int = 0,
)
@@ -72,39 +77,15 @@ data class Locations (
var lat : Double,
var lon : Double,
var street : String = "",
val search_filter: SearchFilter,
val search_filter: String,
)
@Serializable
data class SearchFilter(
var max_road_class: String = "",
var exclude_toll : Boolean = false
) {
var avoidMotorway: Boolean = false,
var avoidTollway : Boolean = false,
class Builder {
private var avoidMotorway = false
private var avoidTollway = false
)
fun avoidMotorway (value: Boolean ) = apply {
avoidMotorway = value
}
fun avoidTollway (value: Boolean ) = apply {
avoidTollway = value
}
fun build(): SearchFilter {
val filter = SearchFilter()
if (avoidMotorway) {
filter.max_road_class = "trunk"
}
if (avoidTollway) {
filter.exclude_toll = true
}
return filter
}
}
}
@Serializable
data class ValhallaLocation (
@@ -115,13 +96,6 @@ data class ValhallaLocation (
var language: String
)
data class BoundingBox (
var southernLat : Double,
var westernLon: Double,
var northerLat : Double,
var easternLon : Double
)
object Constants {
//const val STYLE: String = "https://kouros-online.de/liberty.json"
@@ -149,17 +123,8 @@ object Constants {
val categories = listOf("Tankstelle", "Apotheke", "Ladestationen")
/** The initial location to use as an anchor for searches. */
val homeLocation: Location = Location(LocationManager.GPS_PROVIDER)
val home2Location: Location = Location(LocationManager.GPS_PROVIDER)
init {
// Vogelhartstr. 17
homeLocation.latitude = 48.185749
homeLocation.longitude = 11.5793748
// Hohenwaldeckstr. 27
home2Location.latitude = 48.1164817
home2Location.longitude = 11.594322
}
val homeVogelhart = location(11.5793748, 48.185749)
val homeHohenwaldeck = location( 11.594322, 48.1164817)
const val SHARED_PREF_KEY = "NavigationPrefs"
@@ -171,18 +136,24 @@ object Constants {
const val AVOID_TOLLWAY = "AvoidTollway"
const val NEXT_STEP_THRESHOLD = 100.0
const val CAR_LOCATION = "CarLocation"
const val ROUTING_ENGINE = "RoutingEngine"
const val NEXT_STEP_THRESHOLD = 120.0
const val MAXIMAL_SNAP_CORRECTION = 50.0
const val MAXIMAL_ROUTE_DEVIATION = 100.0
const val MAXIMAL_ROUTE_DEVIATION = 80.0
const val DESTINATION_ARRIVAL_DISTANCE = 40.0
val ROUTE_ENGINE = RouteEngine.VALHALLA.name
const val NEAREST_LOCATION_DISTANCE = 10F
const val MAXIMUM_LOCATION_DISTANCE = 100000F
}
enum class RouteEngine {
VALHALLA, OSRM, GRAPHHOPPER
VALHALLA, OSRM, TOMTOM, GRAPHHOPPER
}

View File

@@ -18,68 +18,63 @@ package com.kouros.navigation.data
import android.content.Context
import android.location.Location
import com.kouros.navigation.data.overpass.Elements
import com.kouros.data.R
import com.kouros.navigation.model.RouteModel
import org.json.JSONArray
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import java.net.Authenticator
import java.net.HttpURLConnection
import java.net.PasswordAuthentication
import java.net.URL
import kotlinx.serialization.json.Json
abstract class NavigationRepository {
private val placesUrl = "https://kouros-online.de/maps/placespwd";
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
abstract fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String
fun getRouteDistance(currentLocation: Location, location: Location, searchFilter: SearchFilter, context: Context): Double {
val route = getRoute(currentLocation, location, searchFilter)
val routeModel = RouteModel()
routeModel.startNavigation(route, context)
return routeModel.route.summary!!.distance
abstract fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String
abstract fun getTraffic(context: Context, location: Location, carOrientation: Float): String
fun getRouteDistance(
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter,
context: Context
): Double {
val route = getRoute(context, currentLocation, location, carOrientation, searchFilter)
val routeModel = RouteModel()
routeModel.startNavigation(route, context)
return routeModel.curRoute.summary.distance
}
fun searchPlaces(search: String, location: Location) : String {
// val bbox = getBoundingBox(location.longitude, location.latitude, 10.0)
// val neLon = bbox["ne"]?.get("lon")
// val neLat = bbox["ne"]?.get("lat")
// val swLon = bbox["sw"]?.get("lon")
// val swLat = bbox["sw"]?.get("lat")
// val viewbox = "&viewbox=$swLon,$swLat,$neLon,$neLat"
return fetchUrl("${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true,&countrycodes=de", false)
fun searchPlaces(search: String, location: Location): String {
val box = calculateSquareRadius(location.latitude, location.longitude, 100.0)
val viewbox = "&bounded=1&viewbox=${box}"
return fetchUrl(
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
true
)
}
fun reverseAddress(location: Location) : String {
return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true&countrycodes=de", false)
fun reverseAddress(location: Location): String {
return fetchUrl(
"${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true",
false
)
}
fun getPlaces(): List<Place> {
val places: MutableList<Place> = ArrayList()
val placesStr = fetchUrl(placesUrl, true)
val jArray = JSONArray(placesStr)
for (i in 0..<jArray.length()) {
val json = jArray.getJSONObject(i)
val place = Place(
json.getString("id").toLong(),
json.getString("name"),
category = json.getString("category"),
latitude = json.getDouble("latitude"),
longitude = json.getDouble("longitude"),
postalCode = json.getString("postalCode"),
city = json.getString("city"),
street = json.getString("street"),
)
places.add(place)
}
return places
}
fun fetchUrl(url: String, authenticator : Boolean): String {
fun fetchUrl(url: String, authenticator: Boolean): String {
try {
if (authenticator) {
Authenticator.setDefault(object : Authenticator() {
@@ -102,7 +97,7 @@ abstract class NavigationRepository {
val responseCode = httpURLConnection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader()
.use { it.readText() } // defaults to UTF-8
.use { it.readText() }
return response
}
} catch (e: Exception) {

View File

@@ -1,18 +1,16 @@
package com.kouros.navigation.data
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.data.tomtom.TomTomResponse
import com.kouros.navigation.data.tomtom.TomTomRoute
import com.kouros.navigation.data.valhalla.ValhallaResponse
import com.kouros.navigation.data.valhalla.ValhallaRoute
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
import com.kouros.navigation.utils.location
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@@ -23,34 +21,23 @@ import org.maplibre.geojson.Point
data class Route(
val routeEngine: Int,
val summary: Summary?,
val legs: List<Leg>?,
val routeGeoJson: String = "",
val centerLocation: Location = location(0.0, 0.0),
var currentStep: Int = 0,
val waypoints: List<List<Double>>?,
val routes: List<com.kouros.navigation.data.route.Routes>,
var currentStepIndex: Int = 0,
) {
data class Builder(
var routeEngine: Int = 0,
var summary: Summary? = null,
var legs: List<Leg>? = null,
var routeGeoJson: String = "",
var centerLocation: Location = location(0.0, 0.0),
var waypoints: List<List<Double>>? = null,
var summary: Summary = Summary(),
var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(),
) {
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun summary(summary: Summary) = apply { this.summary = summary }
fun legs(legs: List<Leg>) = apply { this.legs = legs }
fun routeGeoJson(routeGeoJson: String) = apply {
this.routeGeoJson = routeGeoJson
centerLocation = createCenterLocation(routeGeoJson)
}
fun routes(routes: List<com.kouros.navigation.data.route.Routes>) = apply {
this.routes = routes
}
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun waypoints(waypoints: List<List<Double>>) = apply { this.waypoints = waypoints }
fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create()
@@ -63,12 +50,15 @@ data class Route(
jsonObject["trip"].toString(),
ValhallaResponse::class.java
)
ValhallaRoute().mapJsonToValhalla(routeJson, this)
ValhallaRoute().mapToRoute(routeJson, this)
}
else -> {
RouteEngine.OSRM.ordinal -> {
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
OsrmRoute().mapToOsrm(osrmJson, this)
OsrmRoute().mapToRoute(osrmJson, this)
}
else -> {
val tomtomJson = gson.fromJson(route, TomTomResponse::class.java)
TomTomRoute().mapToRoute(tomtomJson, this)
}
}
}
@@ -77,17 +67,49 @@ data class Route(
fun build(): Route {
return Route(
routeEngine = this.routeEngine,
summary = this.summary,
legs = this.legs,
waypoints = this.waypoints,
routeGeoJson = this.routeGeoJson,
routes = this.routes,
)
}
fun buildEmpty(): Route {
return Route(
routeEngine = 0,
routes = emptyList(),
)
}
}
fun legs(): List<Leg> {
return if (routes.isNotEmpty()) {
routes.first().legs
} else {
emptyList()
}
}
fun isRouteValid(): Boolean {
return routes.isNotEmpty() && legs().isNotEmpty()
}
fun currentStep(): Step {
return if (isRouteValid()) {
legs().first().steps[currentStepIndex]
} else {
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
}
}
fun nextStep(steps : Int): Step {
val nextIndex = currentStepIndex + steps
return if (isRouteValid() && nextIndex < legs().first().steps.size) {
legs().first().steps[nextIndex]
} else {
currentStep()
}
}
fun maneuverLocations(): List<Point> {
val step = currentStep()
val waypoints = step.maneuver.waypoints
val waypoints = currentStep().maneuver.waypoints
val points = mutableListOf<Point>()
for (loc in waypoints) {
val point = Point.fromLngLat(loc[0], loc[1])
@@ -95,21 +117,4 @@ data class Route(
}
return points
}
fun currentStep(): Step {
if (legs != null) {
return legs.first().steps[currentStep]
} else {
throw IndexOutOfBoundsException("No legs available.")
}
}
fun nextStep(): Step {
val nextIndex = currentStep + 1
return if (nextIndex < legs!!.first().steps.size) {
legs.first().steps[currentStep + 1]
} else {
throw IndexOutOfBoundsException("No next maneuver available.")
}
}
}

View File

@@ -5,7 +5,8 @@ import com.google.gson.annotations.SerializedName
data class Intersections(
@SerializedName("out") var out: Int? = null,
@SerializedName("in") var inV: Int = 0,
@SerializedName("out") var out: Int = 0,
@SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(),
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),

View File

@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
data class Legs (
@SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(),
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("summary" ) var summary : String? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
@SerializedName("steps" ) var steps : List<Steps> = listOf(),
@SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("summary" ) var summary : String = "",
@SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double = 0.0
)

View File

@@ -3,12 +3,13 @@ package com.kouros.navigation.data.osrm
import com.google.gson.annotations.SerializedName
data class Maneuver (
data class Maneuver(
@SerializedName("bearing_after" ) var bearingAfter : Int? = null,
@SerializedName("bearing_before" ) var bearingBefore : Int? = null,
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
@SerializedName("modifier" ) var modifier : String? = null,
@SerializedName("type" ) var type : String? = null
@SerializedName("bearing_after") var bearingAfter: Int = 0,
@SerializedName("bearing_before") var bearingBefore: Int = 0,
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
@SerializedName("modifier") var modifier: String = "",
@SerializedName("type") var type: String = "",
@SerializedName("exit") var exit: Int = 0,
)
)

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.osrm
import android.content.Context
import android.location.Location
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
@@ -8,11 +9,29 @@ private const val routeUrl = "https://kouros-online.de/osrm/route/v1/driving/"
class OsrmRepository : NavigationRepository() {
override fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String {
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true"
return fetchUrl(routeUrl + routeLocation, true)
var exclude = ""
if (searchFilter.avoidMotorway) {
exclude = "&exclude=motorway"
}
if (searchFilter.avoidTollway) {
exclude = "$exclude&exclude=toll"
}
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=0"
return fetchUrl(routeUrl + routeLocation + exclude, true)
}
override fun getTraffic(
context: Context,
location: Location,
carOrientation: Float
): String {
TODO("Not yet implemented")
}
}

View File

@@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
data class OsrmResponse (
@SerializedName("code" ) var code : String? = null,
@SerializedName("code" ) var code : String = "",
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()

View File

@@ -1,44 +1,88 @@
package com.kouros.navigation.data.osrm
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location
class OsrmRoute {
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) {
val waypoints = mutableListOf<List<Double>>()
val summary = Summary()
summary.distance = routeJson.routes.first().distance!!
summary.duration = routeJson.routes.first().duration!!
val steps = mutableListOf<Step>()
fun mapToRoute(routeJson: OsrmResponse, builder: Route.Builder) {
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
var stepIndex = 0
routeJson.routes.first().legs.first().steps.forEach {
if (it.maneuver != null) {
val points = decodePolyline(it.geometry!!, 5)
waypoints.addAll(points)
val maneuver = RouteManeuver(
bearingBefore = it.maneuver!!.bearingBefore ?: 0,
bearingAfter = it.maneuver!!.bearingAfter ?: 0,
type = convertType(it.maneuver!!),
waypoints = points
)
val step = Step( index = stepIndex, name = it.name!!, distance = it.distance!!, duration = it.duration!!, maneuver = maneuver)
steps.add(step)
stepIndex += 1
routeJson.routes.forEach { route ->
val legs = mutableListOf<Leg>()
val waypoints = mutableListOf<List<Double>>()
val summary = Summary(route.duration, route.distance / 1000)
route.legs.forEach { leg ->
val steps = mutableListOf<Step>()
leg.steps.forEach { step ->
val intersections = mutableListOf<Intersection>()
val points = decodePolyline(step.geometry, 5)
waypoints.addAll(points)
val maneuver = RouteManeuver(
bearingBefore = step.maneuver.bearingBefore,
bearingAfter = step.maneuver.bearingAfter,
type = convertType(step.maneuver),
waypoints = points,
exit = step.maneuver.exit,
location = location(
step.maneuver.location[0],
step.maneuver.location[1]
)
)
step.intersections.forEach { it2 ->
if (it2.location[0] != 0.0) {
val lanes = mutableListOf<Lane>()
it2.lanes.forEach { it3 ->
if (it3.indications.isNotEmpty() && it3.indications.first() != "none") {
val lane = Lane(
location(it2.location[0], it2.location[1]),
it3.valid,
it3.indications
)
lanes.add(lane)
}
}
intersections.add(Intersection(it2.location, lanes))
}
}
val step = Step(
index = stepIndex,
street = step.name,
distance = step.distance / 1000,
duration = step.duration,
maneuver = maneuver,
//intersection = intersections
)
steps.add(step)
stepIndex += 1
}
legs.add(Leg(steps))
}
val routeGeoJson = createLineStringCollection(waypoints)
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
val newRoute = com.kouros.navigation.data.route.Routes(
legs,
summary,
routeGeoJson,
centerLocation = centerLocation,
waypoints = waypoints
)
routes.add(newRoute)
}
val leg = Leg(steps)
builder
.routeType(1)
.summary(summary)
.routeGeoJson(createLineStringCollection(waypoints))
.legs(listOf(leg))
.waypoints(waypoints.toList())
.routeType(RouteEngine.OSRM.ordinal)
.routes(routes)
}
fun convertType(maneuver: Maneuver): Int {
@@ -47,38 +91,138 @@ class OsrmRoute {
ManeuverType.depart.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
}
ManeuverType.arrive.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT
}
if (maneuver.modifier == "left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT
}
if (maneuver.modifier == "straight") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_STRAIGHT
}
}
ManeuverType.continue_.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
}
ManeuverType.turn.value -> {
if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
}
if (maneuver.modifier == "left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
}
}
ManeuverType.newName.value -> {
if (maneuver.modifier == "straight") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
}
if (maneuver.modifier == "slight right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
if (maneuver.modifier == "slight left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
}
}
ManeuverType.turn.value,
ManeuverType.endOfRoad.value -> {
if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
}
if (maneuver.modifier == "left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
}
if (maneuver.modifier == "straight") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
}
}
ManeuverType.endOfRoad.value,
ManeuverType.onRamp.value
-> {
if (maneuver.modifier == "left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
}
if (maneuver.modifier == "slight right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
if (maneuver.modifier == "slight left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
}
}
ManeuverType.offRamp.value
-> {
if (maneuver.modifier == "slight right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
}
if (maneuver.modifier == "slight left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
}
if (maneuver.modifier == "left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
}
}
ManeuverType.fork.value
-> {
if (maneuver.modifier == "slight left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
}
if (maneuver.modifier == "slight right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
}
ManeuverType.merge.value
-> {
if (maneuver.modifier == "slight left") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
}
if (maneuver.modifier == "slight right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
}
ManeuverType.roundAbout.value
-> {
if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
}
}
ManeuverType.exitRoundabout.value
-> {
if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
}
}
}
return newType
}
}
enum class ManeuverType(val value: String) {
turn("turn"),
depart("depart"),
arrive("arrive"),
merge("merge"),
onRamp("on ramp"),
offRamp("off ramp"),
fork("fork"),
endOfRoad("end of road"),
continue_("continue"),
roundAbout("roundabout"),
rotary("rotary"),
roundaboutTurn("roundabout turn"),
notification("notification"),
exitRoundabout("exit roundabout"),
exitRotary("exit rotary")
enum class ManeuverType(val value: String) {
turn("turn"),
depart("depart"),
arrive("arrive"),
merge("merge"),
onRamp("on ramp"),
offRamp("off ramp"),
fork("fork"),
endOfRoad("end of road"),
continue_("continue"),
roundAbout("roundabout"),
rotary("rotary"),
roundaboutTurn("roundabout turn"),
notification("notification"),
exitRoundabout("exit roundabout"),
exitRotary("exit rotary"),
newName("new name"),
}
}

View File

@@ -8,8 +8,8 @@ data class Routes (
@SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
@SerializedName("weight_name" ) var weightName : String? = null,
@SerializedName("geometry" ) var geometry : String? = null,
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
@SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double = 0.0
)

View File

@@ -6,13 +6,13 @@ import com.google.gson.annotations.SerializedName
data class Steps (
@SerializedName("intersections" ) var intersections : ArrayList<Intersections> = arrayListOf(),
@SerializedName("driving_side" ) var drivingSide : String? = null,
@SerializedName("geometry" ) var geometry : String? = null,
@SerializedName("maneuver" ) var maneuver : Maneuver? = Maneuver(),
@SerializedName("name" ) var name : String? = null,
@SerializedName("mode" ) var mode : String? = null,
@SerializedName("weight" ) var weight : Double? = null,
@SerializedName("duration" ) var duration : Double? = null,
@SerializedName("distance" ) var distance : Double? = null
@SerializedName("driving_side" ) var drivingSide : String = "",
@SerializedName("geometry" ) var geometry : String = "",
@SerializedName("maneuver" ) val maneuver : Maneuver = Maneuver(),
@SerializedName("name" ) var name : String = "",
@SerializedName("mode" ) var mode : String = "",
@SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double = 0.0,
)

View File

@@ -5,9 +5,9 @@ import com.google.gson.annotations.SerializedName
data class Waypoints (
@SerializedName("hint" ) var hint : String? = null,
@SerializedName("hint" ) var hint : String = "",
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
@SerializedName("name" ) var name : String? = null,
@SerializedName("distance" ) var distance : Double? = null
@SerializedName("name" ) var name : String = "",
@SerializedName("distance" ) var distance : Double = 0.0,
)

View File

@@ -5,10 +5,10 @@ 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("type" ) var type : String = "",
@SerializedName("id" ) var id : Long = 0,
@SerializedName("lat" ) var lat : Double = 0.0,
@SerializedName("lon" ) var lon : Double = 0.0,
@SerializedName("tags" ) var tags : Tags = Tags(),
var distance : Double = 0.0

View File

@@ -2,8 +2,7 @@ package com.kouros.navigation.data.overpass
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.navigation.utils.GeoUtils.getOverpassBbox
import kotlinx.serialization.json.Json
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
@@ -14,7 +13,7 @@ class Overpass {
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
fun getAround(radius: Int, linestring: String) : List<Elements> {
fun getAround(radius: Int, linestring: String): List<Elements> {
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(
@@ -41,7 +40,7 @@ class Overpass {
location: Location,
radius: Double
): List<Elements> {
val boundingBox = getOverpassBbox(location, radius)
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
httpURLConnection.setRequestProperty(
@@ -58,24 +57,29 @@ class Overpass {
| node[$type=$category]
| ($boundingBox);
|);
|(._;>;);
|out body;
""".trimMargin()
return overpassApi(httpURLConnection, searchQuery)
}
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String) : List<Elements> {
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
outputStreamWriter.write(searchQuery)
outputStreamWriter.flush()
// Check if the connection is successful
val responseCode = httpURLConnection.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
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String): List<Elements> {
try {
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
outputStreamWriter.write(searchQuery)
outputStreamWriter.flush()
// Check if the connection is successful
val responseCode = httpURLConnection.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)
return overpass.elements
}
} catch (e: Exception) {
}
return emptyList()
}

View File

@@ -18,7 +18,7 @@ data class Tags(
@SerializedName("ref") var ref: String? = null,
@SerializedName("socket:type2") var socketType2: String? = null,
@SerializedName("socket:type2:output") var socketType2Output: String? = null,
@SerializedName("maxspeed") var maxspeed: String? = null,
@SerializedName("maxspeed") var maxspeed: String = "0",
@SerializedName("direction") var direction: String? = null,
)

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.route
import java.util.Collections
data class Intersection(
val location: List<Double> = listOf(0.0, 0.0),
val lane : List<Lane> = Collections.emptyList<Lane>(),
)

View File

@@ -0,0 +1,9 @@
package com.kouros.navigation.data.route
import android.location.Location
data class Lane (
val location: Location,
val valid: Boolean,
var indications: List<String>,
)

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.route
data class Leg(
var steps : List<Step> = arrayListOf()
var steps : List<Step> = arrayListOf(),
var intersection: List<Intersection> = arrayListOf()
)

View File

@@ -1,8 +1,13 @@
package com.kouros.navigation.data.route
import android.location.Location
data class Maneuver(
val bearingBefore : Int = 0,
val bearingAfter : Int = 0,
val type: Int = 0,
val waypoints: List<List<Double>>,
val location: Location,
val exit: Int = 0,
val street: String = "",
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.route
import android.location.Location
import com.kouros.navigation.utils.location
class Routes(
val legs: List<Leg> = arrayListOf(),
val summary: Summary,
val routeGeoJson: String,
val centerLocation: Location = location(0.0, 0.0),
val waypoints: List<List<Double>>,
)

View File

@@ -1,10 +1,15 @@
package com.kouros.navigation.data.route
class Step(
import android.location.Location
import com.kouros.navigation.utils.location
data class Step(
var index : Int = 0,
var waypointIndex : Int = 0,
var wayPointLocation : Location = location(0.0,0.0),
val maneuver: Maneuver,
val duration: Double = 0.0,
val distance: Double = 0.0,
val name : String = "",
val street : String = "",
val intersection: List<Intersection> = mutableListOf(),
)

View File

@@ -1,6 +1,8 @@
package com.kouros.navigation.data.route
data class Summary(
// sec
var duration : Double = 0.0,
// km
var distance : Double = 0.0,
)

View File

@@ -0,0 +1,5 @@
package com.kouros.navigation.data.tomtom
data class Cause(
val mainCauseCode: Int
)

View File

@@ -0,0 +1,10 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Events (
@SerializedName("description" ) var description : String? = null
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Features (
@SerializedName("type" ) var type : String? = null,
@SerializedName("properties" ) var properties : Properties? = Properties(),
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
)

View File

@@ -0,0 +1,11 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Geometry (
@SerializedName("type" ) var type : String? = null,
@SerializedName("coordinates" ) var coordinates : List<List<Double>> = arrayListOf()
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Guidance(
val instructionGroups: List<InstructionGroup>,
val instructions: List<Instruction>
)

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Incidents (
@SerializedName("type" ) var type : String? = null,
@SerializedName("properties" ) var properties : Properties? = Properties(),
@SerializedName("geometry" ) var geometry : Geometry? = Geometry()
)

View File

@@ -0,0 +1,21 @@
package com.kouros.navigation.data.tomtom
data class Instruction(
val combinedMessage: String,
val countryCode: String,
val drivingSide: String,
val instructionType: String,
val junctionType: String,
val maneuver: String,
val message: String,
val point: Point,
val pointIndex: Int,
val possibleCombineWithNext: Boolean,
val roadNumbers: List<String>,
val routeOffsetInMeters: Int,
val signpostText: String,
val street: String? = "",
val travelTimeInSeconds: Int,
val turnAngleInDecimalDegrees: Int,
val exitNumber: String? = "0",
)

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.tomtom
data class InstructionGroup(
val firstInstructionIndex: Int,
val groupLengthInMeters: Int,
val groupMessage: String,
val lastInstructionIndex: Int
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Lane(
val directions: List<String>,
val follow: String
)

View File

@@ -0,0 +1,7 @@
package com.kouros.navigation.data.tomtom
data class Leg(
val encodedPolyline: String,
val encodedPolylinePrecision: Int,
val summary: SummaryX
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class Point(
val latitude: Double,
val longitude: Double
)

View File

@@ -0,0 +1,11 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Properties (
@SerializedName("iconCategory" ) var iconCategory : Int? = null,
@SerializedName("events" ) var events : ArrayList<Events> = arrayListOf()
)

View File

@@ -0,0 +1,8 @@
package com.kouros.navigation.data.tomtom
data class Route(
val guidance: Guidance,
val legs: List<Leg>,
val sections: List<Section>?,
val summary: SummaryX
)

View File

@@ -0,0 +1,16 @@
package com.kouros.navigation.data.tomtom
import java.util.Collections
data class Section(
val delayInSeconds: Int,
val effectiveSpeedInKmh: Int,
val endPointIndex: Int,
val eventId: String,
val laneSeparators: List<String>,
val lanes: List<Lane>? = Collections.emptyList<Lane>(),
val magnitudeOfDelay: Int,
val sectionType: String,
val simpleCategory: String,
val startPointIndex: Int,
)

View File

@@ -0,0 +1,10 @@
package com.kouros.navigation.data.tomtom
data class SummaryX(
val arrivalTime: String,
val departureTime: String,
val lengthInMeters: Int,
val trafficDelayInSeconds: Int,
val trafficLengthInMeters: Int,
val travelTimeInSeconds: Int
)

View File

@@ -0,0 +1,66 @@
package com.kouros.navigation.data.tomtom
import android.content.Context
import android.location.Location
import com.kouros.data.R
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
private const val routeUrl = "https://api.tomtom.com/routing/1/calculateRoute/"
val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails"
private val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
const val useAsset = false
class TomTomRepository : NavigationRepository() {
override fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String {
if (useAsset) {
val routeJson = context.resources.openRawResource(R.raw.tomom_routing)
val routeJsonString = routeJson.bufferedReader().use { it.readText() }
return routeJsonString
}
val url =
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:${location.latitude},${location.longitude}" +
"/json?vehicleHeading=90&sectionType=traffic&report=effectiveSettings&routeType=eco" +
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
"&instructionsType=text&language=en-GB&sectionType=lanes" +
"&routeRepresentation=encodedPolyline" +
"&vehicleEngineType=combustion&key=$tomtomApiKey"
return fetchUrl(
url,
false
)
}
override fun getTraffic(context: Context, location: Location, carOrientation: Float): String {
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useAsset) {
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)
trafficJson.bufferedReader().use { it.readText() }
} else {
val trafficResult = fetchUrl(
"$tomtomTrafficUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present",
false
)
trafficResult.replace(
"{\"incidents\":",
"{\"type\": \"FeatureCollection\", \"features\":"
)
}
}
}

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class TomTomResponse(
val formatVersion: String,
val routes: List<Route>
)

View File

@@ -0,0 +1,190 @@
package com.kouros.navigation.data.tomtom
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.Intersection
import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
class TomTomRoute {
fun mapToRoute(routeJson: TomTomResponse, builder: Route.Builder) {
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
routeJson.routes.forEach { route ->
val waypoints = mutableListOf<List<Double>>()
val legs = mutableListOf<Leg>()
var stepIndex = 0
var points = listOf<List<Double>>()
val summary = Summary(
route.summary.travelTimeInSeconds.toDouble(),
route.summary.lengthInMeters.toDouble()
)
route.legs.forEach { leg ->
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)
waypoints.addAll(points)
}
var stepDistance = 0.0
var stepDuration = 0.0
val allIntersections = mutableListOf<Intersection>()
val steps = mutableListOf<Step>()
var lastPointIndex = 0
for (index in 1..< route.guidance.instructions.size) {
val lastInstruction = route.guidance.instructions[index-1]
val instruction = route.guidance.instructions[index]
val street = lastInstruction.street ?: ""
val maneuverStreet = instruction.street ?: ""
val maneuver = RouteManeuver(
bearingBefore = 0,
bearingAfter = 0,
type = convertType(instruction.maneuver),
waypoints = points.subList(
lastPointIndex,
instruction.pointIndex+1,
),
exit = exitNumber(instruction),
location = location(
instruction.point.longitude, instruction.point.latitude
),
street = maneuverStreet
)
lastPointIndex = instruction.pointIndex
val intersections = mutableListOf<Intersection>()
route.sections?.forEach { section ->
val lanes = mutableListOf<Lane>()
var startIndex = 0
if (section.startPointIndex <= instruction.pointIndex - 3
&& instruction.pointIndex <= section.endPointIndex
) {
section.lanes?.forEach { itLane ->
val lane = Lane(
location(
waypoints[section.startPointIndex][0],
waypoints[section.startPointIndex][1]
),
itLane.directions.first() == itLane.follow,
itLane.directions
)
startIndex = section.startPointIndex
lanes.add(lane)
}
intersections.add(Intersection(waypoints[startIndex], lanes))
}
}
allIntersections.addAll(intersections)
stepDistance = route.guidance.instructions[index].routeOffsetInMeters - stepDistance
stepDuration = route.guidance.instructions[index].travelTimeInSeconds - stepDuration
val step = Step(
index = stepIndex,
street = street,
distance = stepDistance,
duration = stepDuration,
maneuver = maneuver,
intersection = intersections
)
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
steps.add(step)
stepIndex += 1
}
legs.add(Leg(steps, allIntersections))
val routeGeoJson = createLineStringCollection(waypoints)
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
val newRoute = com.kouros.navigation.data.route.Routes(
legs,
summary,
routeGeoJson,
centerLocation = centerLocation,
waypoints = waypoints
)
routes.add(newRoute)
}
builder
.routeType(RouteEngine.TOMTOM.ordinal)
.routes(routes)
}
fun convertType(type: String): Int {
var newType = 0
when (type) {
"DEPART" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
}
"ARRIVE" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
}
"ARRIVE_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT
}
"ARRIVE_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT
}
"STRAIGHT", "FOLLOW" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
}
"KEEP_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_RIGHT
}
"BEAR_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
"BEAR_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
}
"KEEP_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_KEEP_LEFT
}
"TURN_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
}
"TURN_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
}
"SHARP_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_LEFT
}
"SHARP_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_RIGHT
}
"ROUNDABOUT_RIGHT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
}
"ROUNDABOUT_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CW
}
}
return newType
}
}
private fun exitNumber(
instruction: Instruction
): Int {
return if ( instruction.exitNumber == null
|| instruction.exitNumber.isEmpty()) {
0
} else {
instruction.exitNumber.toInt()
}
}

View File

@@ -0,0 +1,12 @@
package com.kouros.navigation.data.tomtom
import com.google.gson.annotations.SerializedName
data class Traffic (
//@SerializedName("incidents" ) var incidents : ArrayList<Incidents> = arrayListOf()
@SerializedName("type" ) var type : String = "",
@SerializedName("features" ) var features : ArrayList<Features> = arrayListOf()
)

View File

@@ -0,0 +1,6 @@
package com.kouros.navigation.data.tomtom
data class TrafficData (
var traffic : Traffic ,
var trafficData: String = ""
)

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.valhalla
import android.content.Context
import android.location.Location
import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository
@@ -12,11 +13,29 @@ private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
class ValhallaRepository : NavigationRepository() {
override fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
SearchFilter
override fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String {
var exclude = ""
if (searchFilter.avoidMotorway) {
exclude = "&max_road_class='trunk'"
}
if (searchFilter.avoidTollway) {
exclude = "$exclude&exclude_toll=true"
}
val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
Locations(
lat = currentLocation.latitude,
lon = currentLocation.longitude,
search_filter = exclude
),
Locations(lat = location.latitude, lon = location.longitude, search_filter = exclude)
)
val valhallaLocation = ValhallaLocation(
locations = vLocation,
@@ -28,4 +47,12 @@ class ValhallaRepository : NavigationRepository() {
val routeLocation = Json.encodeToString(valhallaLocation)
return fetchUrl(routeUrl + routeLocation, true)
}
override fun getTraffic(
context: Context,
location: Location,
carOrientation: Float
): String {
TODO("Not yet implemented")
}
}

View File

@@ -1,43 +1,98 @@
package com.kouros.navigation.data.valhalla
import androidx.car.app.navigation.model.Maneuver
import com.kouros.data.R
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Maneuver
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location
class ValhallaRoute {
fun mapJsonToValhalla(routeJson: ValhallaResponse, builder: Route.Builder) {
fun mapToRoute(routeJson: ValhallaResponse, builder: Route.Builder) {
val waypoints = decodePolyline(routeJson.legs[0].shape)
val summary = Summary()
summary.distance = routeJson.summaryValhalla.length
summary.duration = routeJson.summaryValhalla.time
val summary = Summary(routeJson.summaryValhalla.time, routeJson.summaryValhalla.length)
val steps = mutableListOf<Step>()
var stepIndex = 0
routeJson.legs[0].maneuvers.forEach {
val maneuver = Maneuver(
val maneuver = RouteManeuver(
bearingBefore = 0,
bearingAfter = it.bearingAfter,
type = it.type,
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1)
//type = it.type,
type = convertType(it),
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1),
// TODO: calculate from ShapeIndex !
location = location(0.0, 0.0)
)
var name = ""
if (it.streetNames != null && it.streetNames.isNotEmpty()) {
name = it.streetNames[0]
}
val step = Step( index = stepIndex, name = name, distance = it.length, duration = it.time, maneuver = maneuver)
val step = Step( index = stepIndex, street = name, distance = it.length, duration = it.time, maneuver = maneuver)
steps.add(step)
stepIndex += 1
}
val leg = Leg(steps)
builder
.routeType(1)
.summary(summary)
.routeGeoJson(createLineStringCollection(waypoints))
.legs(listOf(leg))
.waypoints(waypoints)
.routeType(RouteEngine.VALHALLA.ordinal)
// TODO
.routes(emptyList())
}
fun convertType(maneuver: Maneuvers): Int {
var newType = 0
when (maneuver.type) {
ManeuverType.None.value -> {
newType = Maneuver.TYPE_STRAIGHT
}
ManeuverType.Destination.value,
ManeuverType.DestinationRight.value,
ManeuverType.DestinationLeft.value,
-> {
newType = Maneuver.TYPE_DESTINATION
}
ManeuverType.Right.value -> {
newType = Maneuver.TYPE_TURN_NORMAL_RIGHT
}
ManeuverType.Left.value -> {
newType = Maneuver.TYPE_TURN_NORMAL_LEFT
}
ManeuverType.RampRight.value -> {
newType = Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT
}
ManeuverType.RampLeft.value -> {
newType = Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT
}
ManeuverType.ExitRight.value -> {
newType = Maneuver.TYPE_TURN_SLIGHT_RIGHT
}
ManeuverType.StayRight.value -> {
newType = Maneuver.TYPE_KEEP_RIGHT
}
ManeuverType.StayLeft.value -> {
newType = Maneuver.TYPE_KEEP_LEFT
}
ManeuverType.RoundaboutEnter.value -> {
newType = Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
}
ManeuverType.RoundaboutExit.value -> {
newType = Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
}
}
return newType
}
}

View File

@@ -0,0 +1,32 @@
package com.kouros.navigation.model
import android.content.Context
import android.content.res.Configuration
import androidx.compose.foundation.isSystemInDarkTheme
import com.kouros.data.R
import org.maplibre.compose.style.BaseStyle
class BaseStyleModel {
fun isDarkTheme(context: Context): Boolean {
return context.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
fun readStyle(context: Context, darkModeSettings: Int, isCarDarkMode: Boolean): BaseStyle.Json {
val liberty = when(darkModeSettings) {
0 -> context.resources.openRawResource(R.raw.liberty)
1 -> context.resources.openRawResource(R.raw.liberty_night)
else -> {
if (isDarkTheme(context) || isCarDarkMode) {
context.resources.openRawResource(R.raw.liberty_night)
} else {
context.resources.openRawResource(R.raw.liberty)
}
}
}
val libertyString = liberty.bufferedReader().use { it.readText() }
val baseStyle = BaseStyle.Json(libertyString)
return baseStyle
}
}

View File

@@ -0,0 +1,275 @@
package com.kouros.navigation.model
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Matrix
import androidx.annotation.DrawableRes
import androidx.car.app.model.CarIcon
import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.StepData
import java.util.Collections
import java.util.Locale
import kotlin.collections.forEach
class IconMapper() {
fun maneuverIcon(routeManeuverType: Int): Int {
var currentTurnIcon = R.drawable.ic_turn_name_change
when (routeManeuverType) {
Maneuver.TYPE_STRAIGHT -> {
currentTurnIcon = R.drawable.ic_turn_name_change
}
Maneuver.TYPE_DESTINATION,
Maneuver.TYPE_DESTINATION_RIGHT,
Maneuver.TYPE_DESTINATION_LEFT,
Maneuver.TYPE_DESTINATION_STRAIGHT
-> {
currentTurnIcon = R.drawable.ic_turn_destination
}
Maneuver.TYPE_TURN_NORMAL_RIGHT -> {
currentTurnIcon = R.drawable.ic_turn_normal_right
}
Maneuver.TYPE_TURN_NORMAL_LEFT -> {
currentTurnIcon = R.drawable.ic_turn_normal_left
}
Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT -> {
currentTurnIcon = R.drawable.ic_turn_slight_right
}
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> {
currentTurnIcon = R.drawable.ic_turn_slight_right
}
Maneuver.TYPE_KEEP_RIGHT -> {
currentTurnIcon = R.drawable.ic_turn_name_change
}
Maneuver.TYPE_KEEP_LEFT -> {
currentTurnIcon = R.drawable.ic_turn_name_change
}
Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> {
currentTurnIcon = R.drawable.ic_roundabout_ccw
}
Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> {
currentTurnIcon = R.drawable.ic_roundabout_ccw
}
}
return currentTurnIcon
}
fun addLanes(stepData: StepData) : Int {
stepData.lane.forEach {
if (it.indications.isNotEmpty() && it.valid) {
Collections.sort<String>(it.indications)
var direction = ""
it.indications.forEach { it2 ->
direction = if (direction.isEmpty()) {
it2.trim()
} else {
"${direction}_${it2.trim()}"
}
}
val laneDirection = addLanes(direction, stepData)
return laneDirection
}
}
return 0
}
fun addLanes(direction: String, stepData: StepData): Int {
val laneDirection = when (direction.lowercase(Locale.getDefault())) {
"left_straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"left" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_NORMAL_LEFT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right_straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"left_slight", "slight_left" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
"right_slight", "slight_right" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> LaneDirection.SHAPE_NORMAL_RIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
}
else -> {
LaneDirection.SHAPE_UNKNOWN
}
}
return laneDirection
}
fun createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build()
}
fun createCarIconx(carContext: Context, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}
fun createLaneIcon(context: Context, stepData: StepData): IconCompat {
val bitmaps = mutableListOf<Bitmap>()
stepData.lane.forEach {
if (it.indications.isNotEmpty()) {
Collections.sort<String>(it.indications)
val resource = laneToResource(it.indications, stepData)
if (resource.isNotEmpty()) {
val id = resourceId(resource);
val bitMap = BitmapFactory.decodeResource(context.resources, id)
bitmaps.add(bitMap)
}
}
}
return if (bitmaps.isEmpty()) {
IconCompat.createWithResource(context, R.drawable.ic_close_white_24dp)
} else {
IconCompat.createWithBitmap(overlay(bitmaps = bitmaps))
}
}
fun overlay(bitmaps: List<Bitmap>): Bitmap {
val matrix = Matrix()
if (bitmaps.size == 1) {
return bitmaps.first()
}
val bmOverlay = createBitmap(
bitmaps.first().getWidth() * (bitmaps.size * 1.5).toInt(),
bitmaps.first().getHeight(),
bitmaps.first().getConfig()!!
)
val canvas = Canvas(bmOverlay)
canvas.drawBitmap(bitmaps.first(), matrix, null)
var i = 0
bitmaps.forEach {
if (i > 0) {
matrix.setTranslate(i * 45F, 0F)
canvas.drawBitmap(it, matrix, null)
}
i++
}
return bmOverlay
}
private fun laneToResource(directions: List<String>, stepData: StepData): String {
var direction = ""
directions.forEach {
direction = if (direction.isEmpty()) {
it.trim()
} else {
"${direction}_${it.trim()}"
}
}
direction = direction.lowercase()
return when (direction) {
"left_straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_LEFT -> "left_o_straight_x"
Maneuver.TYPE_STRAIGHT -> "left_x_straight_o"
else
-> "left_x_straight_x"
}
}
"right_straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_NORMAL_RIGHT -> "right_x_straight_x"
Maneuver.TYPE_STRAIGHT -> "right_x_straight_o"
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> "right_o_straight_o"
else
-> "right_x_straight_x"
}
}
"right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_o" else "${direction}_x"
"left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_NORMAL_LEFT) "${direction}_o" else "${direction}_x"
"straight" -> if (stepData.currentManeuverType == Maneuver.TYPE_STRAIGHT) "${direction}_o" else "${direction}_x"
"right_slight", "slight_right" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT) "${direction}_o" else "${direction}_x"
"left_slight", "slight_left" -> if (stepData.currentManeuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT) "${direction}_o" else "${direction}_x"
else -> {
""
}
}
}
fun resourceId(
variableName: String,
): Int {
return when (variableName) {
"left_x" -> R.drawable.left_x
"left_o" -> R.drawable.left_o
"left_o_right_x" -> R.drawable.left_o_right_x
"right_x" -> R.drawable.right_x
"right_o" -> R.drawable.right_o
"slight_right_x" -> R.drawable.slight_right_x
"slight_right_o" -> R.drawable.slight_right_o
"slight_left_x" -> R.drawable.left_x
"straight_x" -> R.drawable.straight_x
"right_o_straight_x" -> R.drawable.right_o_straight_x
"right_x_straight_x" -> R.drawable.right_x_straight_x
"right_x_straight_o" -> R.drawable.right_x_straight_x
"straight_o" -> R.drawable.straight_o
"left_o_straight_x" -> R.drawable.left_o_straight_x
"left_x_straight_o" -> R.drawable.left_x_straight_o
else -> {
R.drawable.left_x
}
}
}
}

View File

@@ -0,0 +1,104 @@
package com.kouros.navigation.model
import android.location.Location
import androidx.car.app.navigation.model.Step
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE
import com.kouros.navigation.utils.location
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
class RouteCalculator(var routeModel: RouteModel) {
var lastSpeedLocation: Location = location(0.0, 0.0)
var lastSpeedIndex: Int = 0
fun findStep(location: Location) {
var nearestDistance = MAXIMUM_LOCATION_DISTANCE
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
if (index >= routeModel.navState.route.currentStepIndex) {
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
if (wayIndex >= step.waypointIndex) {
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
if (distance < nearestDistance) {
nearestDistance = distance
routeModel.navState.route.currentStepIndex = step.index
step.waypointIndex = wayIndex
step.wayPointLocation = location(waypoint[0], waypoint[1])
routeModel.navState = routeModel.navState.copy(
routeBearing = routeModel.navState.lastLocation.bearingTo(location)
)
}
}
}
}
if (nearestDistance < NEAREST_LOCATION_DISTANCE) {
break;
}
}
}
fun travelLeftTime(): Double {
var timeLeft = 0.0
// time for next step until end step
for (i in routeModel.route.currentStepIndex + 1..<routeModel.curLeg.steps.size) {
val step = routeModel.curLeg.steps[i]
timeLeft += step.duration
}
// time for current step
val step = routeModel.route.currentStep()
val curTime = step.duration
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
val time = curTime * percent / 100
timeLeft += time
return timeLeft
}
/** Returns the current [Step] left distance in m. */
fun leftStepDistance(): Double {
val step = routeModel.route.currentStep()
var leftDistance = 0F
for (i in step.waypointIndex..<step.maneuver.waypoints.size - 1) {
val loc1 = location(step.maneuver.waypoints[i][0], step.maneuver.waypoints[i][1])
val loc2 =
location(step.maneuver.waypoints[i + 1][0], step.maneuver.waypoints[i + 1][1])
val distance = loc1.distanceTo(loc2)
leftDistance += distance
}
return (leftDistance / 10.0).roundToInt() * 10.0
}
/** Returns the left distance in m. */
fun travelLeftDistance(): Double {
var leftDistance = 0.0
for (i in routeModel.route.currentStepIndex + 1..<routeModel.curLeg.steps.size) {
val step = routeModel.route.legs()[0].steps[i]
leftDistance += step.distance
}
leftDistance += leftStepDistance()
return leftDistance
}
fun arrivalTime(): Long {
val timeLeft = travelLeftTime()
// Calculate the time to destination from the current time.
val nowUtcMillis = System.currentTimeMillis()
val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
return nowUtcMillis + timeToDestinationMillis
}
fun updateSpeedLimit(location: Location, viewModel: ViewModel) {
if (routeModel.isNavigating()) {
// speed limit
val distance = lastSpeedLocation.distanceTo(location)
if (distance > 500 || lastSpeedIndex < routeModel.route.currentStepIndex) {
lastSpeedIndex = routeModel.route.currentStepIndex
lastSpeedLocation = location
viewModel.getMaxSpeed(location, routeModel.route.currentStep().street)
}
}
}
}

View File

@@ -3,318 +3,155 @@ package com.kouros.navigation.model
import android.content.Context
import android.location.Location
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step
import com.kouros.data.R
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.valhalla.ManeuverType
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.math.absoluteValue
open class RouteModel() {
data class RouteState(
val route: Route? = null,
val isNavigating: Boolean = false,
val destination: Place = Place(),
open class RouteModel {
// Immutable Data Class
data class NavigationState(
val route: Route = Route.Builder().buildEmpty(),
val iconMapper : IconMapper = IconMapper(),
val navigating: Boolean = false,
val arrived: Boolean = false,
val maneuverType: Int = 0,
val travelMessage: String = "",
val lastSpeedLocation: Location = location(0.0, 0.0),
val lastSpeedIndex: Int = 0,
val maxSpeed: Int = 0,
val maneuverType: Int = 0,
val lastLocation: Location = location(0.0, 0.0),
val currentLocation: Location = location(0.0, 0.0),
val routeBearing: Float = 0F,
val currentRouteIndex: Int = 0,
val destination: Place = Place()
)
var routeState = RouteState()
var navState = NavigationState()
var route: Route
get() = routeState.route!!
set(value) {
routeState = routeState.copy(route = value)
}
val route: Route
get() = navState.route
val routeCalculator : RouteCalculator = RouteCalculator(this)
val legs: Leg
get() = routeState.route!!.legs!!.first()
val curRoute: Routes
get() = navState.route.routes[navState.currentRouteIndex]
val curLeg: Leg
get() = navState.route.routes[navState.currentRouteIndex].legs.first()
fun startNavigation(routeString: String, context: Context) {
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE)
val newRoute = Route.Builder()
.routeEngine(routeEngine)
.route(routeString)
.build()
this.routeState = routeState.copy(
route = newRoute,
isNavigating = true
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
navState = navState.copy(
route = Route.Builder()
.routeEngine(routeEngine)
.route(routeString)
.build()
)
if (hasLegs()) {
navState = navState.copy(navigating = true)
}
}
private fun hasLegs(): Boolean {
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty()
}
fun stopNavigation() {
this.routeState = routeState.copy(
route = null,
isNavigating = false,
navState = navState.copy(
route = Route.Builder().buildEmpty(),
navigating = false,
arrived = false,
maneuverType = 0,
maneuverType = Maneuver.TYPE_UNKNOWN
)
}
@OptIn(DelicateCoroutinesApi::class)
fun updateLocation(location: Location, viewModel: ViewModel) {
findStep(location)
updateSpeedLimit(location, viewModel)
fun updateLocation(curLocation: Location, viewModel: ViewModel) {
navState = navState.copy(currentLocation = curLocation)
routeCalculator.findStep(curLocation)
routeCalculator.updateSpeedLimit(curLocation, viewModel)
navState = navState.copy(lastLocation = navState.currentLocation)
}
private fun findStep(location: Location) {
var nearestDistance = 100000.0f
for ((index, step) in legs.steps.withIndex()) {
if (index >= route.currentStep) {
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
if (wayIndex >= step.waypointIndex) {
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
if (distance < nearestDistance) {
nearestDistance = distance
route.currentStep = step.index
step.waypointIndex = wayIndex
}
}
if (nearestDistance == 0F) {
break
}
}
}
if (nearestDistance == 0F) {
break
}
}
//println("Current Index ${route.currentStep} WayPoint: ${route.currentStep().waypointIndex}")
}
fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking {
CoroutineScope(Dispatchers.IO).launch {
// speed limit
val distance = routeState.lastSpeedLocation.distanceTo(location)
if (distance > 500 || routeState.lastSpeedIndex < route.currentStep) {
routeState = routeState.copy(lastSpeedIndex = route.currentStep)
routeState = routeState.copy(lastSpeedLocation = location)
val elements = viewModel.getMaxSpeed(location)
elements.forEach {
if (it.tags.name != null && it.tags.maxspeed != null) {
val speed = it.tags.maxspeed!!.toInt()
routeState = routeState.copy(maxSpeed = speed)
private fun currentLanes(): List<Lane> {
var lanes = emptyList<Lane>()
if (navState.route.legs().isNotEmpty()) {
navState.route.legs().first().intersection.forEach {
if (it.lane.isNotEmpty()) {
val distance =
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
val sectionBearing =
navState.lastLocation.bearingTo(location(it.location[0], it.location[1]))
if (distance < 500 && (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
lanes = it.lane
}
}
}
}
return lanes
}
fun currentStep(): StepData {
val currentStep = route.currentStep()
// Determine if we should display the current or the next maneuver
val distanceToNextStep = leftStepDistance()
val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD
val shouldAdvance =
isNearNextManeuver && route.currentStep < (route.legs!!.first().steps.size)
val distanceToNextStep = routeCalculator.leftStepDistance()
// Determine the maneuver type and corresponding icon
var maneuverType = if (hasArrived(currentStep.maneuver.type)) {
currentStep.maneuver.type
} else {
ManeuverType.None.value
}
// Get the single, correct maneuver for this state
val relevantManeuver = if (shouldAdvance) {
route.nextStep() // This advances the route's state
} else {
route.currentStep()
}
val currentStep = navState.route.nextStep(0)
// Safely get the street name from the maneuver
val streetName = relevantManeuver.name
if (shouldAdvance) {
maneuverType = relevantManeuver.maneuver.type
}
val maneuverIconPair = maneuverIcon(maneuverType)
routeState = routeState.copy(maneuverType = maneuverIconPair.first)
val streetName = currentStep.maneuver.street
val curManeuverType = currentStep.maneuver.type
val exitNumber = currentStep.maneuver.exit
val maneuverIcon = navState.iconMapper.maneuverIcon(curManeuverType)
navState = navState.copy(maneuverType = curManeuverType)
val lanes = currentLanes()
// Construct and return the final StepData object
return StepData(
streetName,
distanceToNextStep,
maneuverIconPair.first,
maneuverIconPair.second,
arrivalTime(),
travelLeftDistance()
navState.maneuverType,
maneuverIcon,
routeCalculator.arrivalTime(),
routeCalculator.travelLeftDistance(),
lanes,
exitNumber
)
}
fun nextStep(): StepData {
val step = route.nextStep()
val step = navState.route.nextStep(1)
val maneuverType = step.maneuver.type
val distanceLeft = leftStepDistance()
val distanceLeft = routeCalculator.leftStepDistance()
var text = ""
when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
}
else -> {
if (step.name.isNotEmpty()) {
text = step.name
if (step.street.isNotEmpty()) {
text = step.street
}
}
}
val routing: (Pair<Int, Int>) = maneuverIcon(maneuverType)
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
text,
distanceLeft,
routing.first,
routing.second,
arrivalTime(),
travelLeftDistance()
maneuverType,
maneuverIcon,
routeCalculator.arrivalTime(),
routeCalculator.travelLeftDistance(),
listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
step.maneuver.exit
)
}
fun travelLeftTime(): Double {
var timeLeft = 0.0
// time for next step until end step
for (i in route.currentStep + 1..<legs.steps.size) {
val step = legs.steps[i]
timeLeft += step.duration
}
// time for current step
val step = route.currentStep()
val curTime = step.duration
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
val time = curTime * percent / 100
timeLeft += time
return timeLeft
}
fun arrivalTime(): Long {
val timeLeft = travelLeftTime()
// Calculate the time to destination from the current time.
val nowUtcMillis = System.currentTimeMillis()
val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
return nowUtcMillis + timeToDestinationMillis
}
/** Returns the current [Step] left distance in m. */
fun leftStepDistance(): Double {
val step = route.currentStep()
var leftDistance = step.distance
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
leftDistance = leftDistance * percent / 100
// The remaining distance to the step, rounded to the nearest 10 units.
return (leftDistance * 1000 / 10.0).roundToInt() * 10.0
}
/** Returns the left distance in km. */
fun travelLeftDistance(): Double {
var leftDistance = 0.0
for (i in route.currentStep + 1..<legs.steps.size) {
val step = route.legs!![0].steps[i]
leftDistance += step.distance
}
val step = route.currentStep()
val curDistance = step.distance
val percent =
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
val distance = curDistance * percent / 100
leftDistance += distance
return leftDistance
}
fun maneuverIcon(routeManeuverType: Int): (Pair<Int, Int>) {
var type = Maneuver.TYPE_DEPART
var currentTurnIcon = R.drawable.ic_turn_name_change
when (routeManeuverType) {
ManeuverType.None.value -> {
type = Maneuver.TYPE_STRAIGHT
currentTurnIcon = R.drawable.ic_turn_name_change
}
ManeuverType.Destination.value,
ManeuverType.DestinationRight.value,
ManeuverType.DestinationLeft.value,
-> {
type = Maneuver.TYPE_DESTINATION
currentTurnIcon = R.drawable.ic_turn_destination
}
ManeuverType.Right.value -> {
type = Maneuver.TYPE_TURN_NORMAL_RIGHT
currentTurnIcon = R.drawable.ic_turn_normal_right
}
ManeuverType.Left.value -> {
type = Maneuver.TYPE_TURN_NORMAL_LEFT
currentTurnIcon = R.drawable.ic_turn_normal_left
}
ManeuverType.RampRight.value -> {
type = Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT
currentTurnIcon = R.drawable.ic_turn_slight_right
}
ManeuverType.RampLeft.value -> {
type = Maneuver.TYPE_TURN_NORMAL_LEFT
currentTurnIcon = R.drawable.ic_turn_normal_left
}
ManeuverType.ExitRight.value -> {
type = Maneuver.TYPE_TURN_SLIGHT_RIGHT
currentTurnIcon = R.drawable.ic_turn_slight_right
}
ManeuverType.StayRight.value -> {
type = Maneuver.TYPE_KEEP_RIGHT
currentTurnIcon = R.drawable.ic_turn_name_change
}
ManeuverType.StayLeft.value -> {
type = Maneuver.TYPE_KEEP_LEFT
currentTurnIcon = R.drawable.ic_turn_name_change
}
ManeuverType.RoundaboutEnter.value -> {
type = Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
currentTurnIcon = R.drawable.ic_roundabout_ccw
}
ManeuverType.RoundaboutExit.value -> {
type = Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
currentTurnIcon = R.drawable.ic_roundabout_ccw
}
}
return Pair(type, currentTurnIcon)
}
fun isNavigating(): Boolean {
return routeState.isNavigating
}
fun hasArrived(type: Int): Boolean {
return type == ManeuverType.DestinationRight.value
|| type == ManeuverType.Destination.value
|| type == ManeuverType.DestinationLeft.value
return navState.navigating
}
}

View File

@@ -18,11 +18,17 @@ import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.data.tomtom.Features
import com.kouros.navigation.data.tomtom.Traffic
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.Levenshtein
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.location
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.maplibre.geojson.FeatureCollection
import java.time.LocalDateTime
import java.time.ZoneOffset
@@ -32,6 +38,11 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData()
}
val traffic: MutableLiveData<Map<String, String>> by lazy {
MutableLiveData()
}
val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData()
}
@@ -54,7 +65,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val placeLocation: MutableLiveData<SearchResult> by lazy {
MutableLiveData()
}
val contactAddress: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
@@ -67,7 +78,14 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData()
}
fun loadRecentPlace(location: Location) {
val maxSpeed: MutableLiveData<Int> by lazy {
MutableLiveData()
}
val routingEngine: MutableLiveData<Int> by lazy {
MutableLiveData()
}
fun loadRecentPlace(location: Location, carOrientation: Float, context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
@@ -79,12 +97,18 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
query.close()
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
//val distance = repository.getRouteDistance(location, plLocation, SearchFilter())
//place.distance = distance.toFloat()
//if (place.distance == 0F) {
recentPlace.postValue(place)
return@launch
//}
val distance = repository.getRouteDistance(
location,
plLocation,
carOrientation,
SearchFilter(),
context
)
place.distance = distance.toFloat()
if (place.distance > 1F) {
recentPlace.postValue(place)
return@launch
}
}
} catch (e: Exception) {
e.printStackTrace()
@@ -92,7 +116,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun loadRecentPlaces(context: Context, location: Location) {
fun loadRecentPlaces(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
@@ -104,15 +128,16 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
query.close()
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) {
val distance =
repository.getRouteDistance(
location,
plLocation,
getSearchFilter(context), context
)
place.distance = distance.toFloat()
}
if (place.latitude != 0.0) {
val distance =
repository.getRouteDistance(
location,
plLocation,
carOrientation,
getSearchFilter(context), context
)
place.distance = distance.toFloat()
}
}
places.postValue(results)
} catch (e: Exception) {
@@ -121,7 +146,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun loadFavorites(context: Context, location: Location) {
fun loadFavorites(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
@@ -134,7 +159,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context)
repository.getRouteDistance(
location,
plLocation,
carOrientation,
getSearchFilter(context),
context
)
place.distance = distance.toFloat()
}
favorites.postValue(results)
@@ -144,13 +175,20 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun loadRoute(context: Context, currentLocation: Location, location: Location) {
fun loadRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float
) {
viewModelScope.launch(Dispatchers.IO) {
try {
route.postValue(
repository.getRoute(
context,
currentLocation,
location,
carOrientation,
getSearchFilter(context)
)
)
@@ -160,13 +198,60 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location) {
fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
val data = repository.getTraffic(
context,
currentLocation,
carOrientation
)
val trafficData = rebuildTraffic(data)
traffic.postValue(
trafficData
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun rebuildTraffic(data: String): Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
val stationary = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
val slow = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
val heavy = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
val roadworks = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
return incidents
}
fun loadPreviewRoute(
context: Context,
currentLocation: Location,
location: Location,
carOrientation: Float
) {
viewModelScope.launch(Dispatchers.IO) {
try {
previewRoute.postValue(
repository.getRoute(
context,
currentLocation,
location,
carOrientation,
getSearchFilter(context)
)
)
@@ -224,21 +309,24 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
}
fun searchPlaces(search: String, location: Location) {
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)
if (placesJson.isNotEmpty()) {
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)
}
val sortedList = distPlaces.sortedWith(compareBy { it.distance })
searchPlaces.postValue(sortedList)
}
val sortedList = distPlaces.sortedWith(compareBy { it.distance })
searchPlaces.postValue(sortedList)
}
}
@@ -265,13 +353,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun getSpeedCameras(location: Location, radius : Double) {
fun getSpeedCameras(location: Location, radius: Double) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
val distAmenities = mutableListOf<Elements>()
amenities.forEach {
val plLocation =
location(longitude = it.lon!!, latitude = it.lat!!)
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
distAmenities.add(it)
@@ -281,10 +369,22 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun getMaxSpeed(location: Location) : List<Elements> {
fun getMaxSpeed(location: Location, street: String) {
viewModelScope.launch(Dispatchers.IO) {
val levenshtein = Levenshtein()
val lineString = "${location.latitude},${location.longitude}"
val amenities = Overpass().getAround(10, lineString)
return amenities
amenities.forEach {
if (it.tags.name != null) {
val distance =
levenshtein.distance(it.tags.name!!, street)
if (distance < 5) {
val speed = it.tags.maxspeed.toInt()
maxSpeed.postValue(speed)
}
}
}
}
}
fun saveFavorite(place: Place) {
@@ -293,6 +393,12 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
fun saveRecent(place: Place) {
if (place.category == Constants.FUEL_STATION
|| place.category == Constants.CHARGING_STATION
|| place.category == Constants.PHARMACY
) {
return
}
place.category = Constants.RECENT
savePlace(place)
}
@@ -351,7 +457,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
fun getSearchFilter(context: Context): SearchFilter {
val avoidMotorway = NavigationUtils.getBooleanKeyValue(
context = context,
Constants.AVOID_MOTORWAY
@@ -360,14 +465,15 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
context = context,
Constants.AVOID_TOLLWAY
)
return SearchFilter.Builder()
.avoidMotorway(avoidMotorway)
.avoidTollway(avoidTollway)
.build()
return SearchFilter(avoidMotorway, avoidTollway)
}
fun loadPlaces2(context: Context, location: Location): SnapshotStateList<Place?> {
fun loadPlaces2(
context: Context,
location: Location,
carOrientation: Float
): SnapshotStateList<Place?> {
val results = listOf<Place>()
try {
val placeBox = boxStore.boxFor(Place::class)
@@ -380,7 +486,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context)
repository.getRouteDistance(
location,
plLocation,
carOrientation,
getSearchFilter(context),
context
)
place.distance = distance.toFloat()
}
} catch (e: Exception) {

View File

@@ -1,7 +1,6 @@
package com.kouros.navigation.utils
import android.location.Location
import com.kouros.navigation.data.BoundingBox
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.maplibre.geojson.FeatureCollection
@@ -10,7 +9,6 @@ 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.dsl.buildMultiPoint
import org.maplibre.spatialk.geojson.toJson
import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc
@@ -33,9 +31,7 @@ object GeoUtils {
return newLocation
}
fun decodePolyline(encoded: String, precision: Int = 6,): List<List<Double>> {
//val precisionOptional = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6
fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> {
val factor = 10.0.pow(precision)
var lat = 0
@@ -93,6 +89,7 @@ object GeoUtils {
}
fun createLineStringCollection(lineCoordinates: List<List<Double>>): String {
// return createPointCollection(lineCoordinates, "Route")
val lineString = buildLineString {
lineCoordinates.forEach {
add(org.maplibre.spatialk.geojson.Point(
@@ -118,40 +115,31 @@ object GeoUtils {
return featureCollection.toJson()
}
fun getOverpassBbox(location: Location, radius: Double): String {
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
val neLon = bbox["ne"]?.get("lon")
val neLat = bbox["ne"]?.get("lat")
val swLon = bbox["sw"]?.get("lon")
val swLat = bbox["sw"]?.get("lat")
return "$swLon,$swLat,$neLon,$neLat"
}
/**
* Calculate the lat and len of a square around a point.
* @return latMin, latMax, lngMin, lngMax
*/
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
val earthRadius = 6371.0 // earth radius in km
val latMin = lat - toDegrees(radius / earthRadius)
val latMax = lat + toDegrees(radius / earthRadius)
val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat)))
val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat)))
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)
return "$lngMin,$latMin,$lngMax,$latMax"
}
fun getBoundingBox(
lat: Double,
lon: Double,
radius: Double
): Map<String, Map<String, Double>> {
): String {
val earthRadius = 6371.0
val maxLat = lat + toDegrees(radius / earthRadius)
val minLat = lat - toDegrees(radius / earthRadius)
val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
return mapOf(
"nw" to mapOf("lat" to maxLat, "lon" to minLon),
"ne" to mapOf("lat" to maxLat, "lon" to maxLon),
"sw" to mapOf("lat" to minLat, "lon" to minLon),
"se" to mapOf("lat" to minLat, "lon" to maxLon)
)
return "$minLat,$minLon,$maxLat,$maxLon"
}
}

View File

@@ -0,0 +1,63 @@
package com.kouros.navigation.utils
import kotlin.math.min
/**
* The Levenshtein distance between two words is the minimum number of single-character edits (insertions, deletions or
* substitutions) required to change one string into the other.
*
* This implementation uses dynamic programming (WagnerFischer algorithm).
*
* [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance)
*/
class Levenshtein {
/**
* The Levenshtein distance, or edit distance, between two words is the minimum number of single-character edits
* (insertions, deletions or substitutions) required to change one word into the other.
*
* It is always at least the difference of the sizes of the two strings.
* It is at most the length of the longer string.
* It is `0` if and only if the strings are equal.
*
* @param first first string to compare.
* @param second second string to compare.
* @param limit the maximum result to compute before stopping, terminating calculation early.
* @return the computed Levenshtein distance.
*/
fun distance(first: CharSequence, second: CharSequence, limit: Int = Int.MAX_VALUE): Int {
if (first == second) return 0
if (first.isEmpty()) return second.length
if (second.isEmpty()) return first.length
// initial costs is the edit distance from an empty string, which corresponds to the characters to inserts.
// the array size is : length + 1 (empty string)
var cost = IntArray(first.length + 1) { it }
var newCost = IntArray(first.length + 1)
for (i in 1..second.length) {
// calculate new costs from the previous row.
// the first element of the new row is the edit distance (deletes) to match empty string
newCost[0] = i
var minCost = i
// fill in the rest of the row
for (j in 1..first.length) {
// if it's the same char at the same position, no edit cost.
val edit = if (first[j - 1] == second[i - 1]) 0 else 1
val replace = cost[j - 1] + edit
val insert = cost[j] + 1
val delete = newCost[j - 1] + 1
newCost[j] = minOf(insert, delete, replace)
minCost = min(minCost, newCost[j])
}
if (minCost >= limit) return limit
// flip references of current and previous row
val swap = cost
cost = newCost
newCost = swap
}
return cost.last()
}
}

View File

@@ -4,10 +4,11 @@ import android.content.Context
import android.location.Location
import android.location.LocationManager
import androidx.core.content.edit
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel
import java.time.LocalDateTime
@@ -25,11 +26,12 @@ import kotlin.time.Duration.Companion.seconds
object NavigationUtils {
fun getRouteEngine(context: Context): ViewModel {
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE)
fun getViewModel(context: Context): ViewModel {
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
return when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
else -> ViewModel(OsrmRepository())
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
else -> ViewModel(TomTomRepository())
}
}
@@ -92,16 +94,16 @@ fun calculateZoom(speed: Double?): Double {
in 61..70 -> 16.5
else -> 16.0
}
return zoom.toDouble()
return zoom
}
fun previewZoom(previewDistance: Double): Double {
when (previewDistance) {
in 0.0..10.0 -> return 13.0
in 10.0..20.0 -> return 11.0
in 20.0..30.0 -> return 10.0
in 0.0..10.0 -> return 13.5
in 10.0..20.0 -> return 11.5
in 20.0..30.0 -> return 10.5
}
return 9.0
return 9.5
}
fun calculateTilt(newZoom: Double, tilt: Double): Double =

View File

@@ -0,0 +1,11 @@
<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"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<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

@@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M337,746L425,606L372,606L372,501L285,641L337,641L337,746ZM220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM220,780L489,780L489,468L220,468L220,780ZM160,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,840ZM489,780L220,780L220,780L489,780Z"/>
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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Some files were not shown because too many files have changed in this diff Show More