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" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 15 versionCode = 38
versionName = "0.1.3.15" versionName = "0.2.0.38"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -94,7 +94,13 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.window) implementation(libs.androidx.window)
implementation(libs.androidx.compose.foundation.layout) 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) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) 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_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>--> <!-- <uses-permission android:name="android.permission.READ_CONTACTS"/>-->
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="MockLocation" /> tools:ignore="MockLocation" />
@@ -20,6 +20,7 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Navigation"> android:theme="@style/Theme.Navigation">
<meta-data <meta-data

View File

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

View File

@@ -48,8 +48,8 @@ class MockLocation (private var locationManager: LocationManager) {
this.latitude = latitude this.latitude = latitude
this.longitude = longitude this.longitude = longitude
this.altitude = 0.0 this.altitude = 0.0
this.accuracy = 1.0f this.accuracy = 0f
this.speed = 10f this.speed = 0f
this.time = System.currentTimeMillis() this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -71,8 +71,8 @@ class MockLocation (private var locationManager: LocationManager) {
this.latitude = latitude this.latitude = latitude
this.longitude = longitude this.longitude = longitude
this.altitude = 0.0 this.altitude = 0.0
this.accuracy = 1.0f this.accuracy = 0f
this.speed = 10f this.speed = 0f
this.time = System.currentTimeMillis() this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() 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.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AppOpsManager import android.app.AppOpsManager
import android.content.Context
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
@@ -14,9 +15,13 @@ import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -32,34 +37,47 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer 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.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel 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.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.data.StepData
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.MockLocation import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.ui.theme.NavigationTheme 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.bearing
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.location 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.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.joda.time.DateTime
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.Location import org.maplibre.compose.location.Location
import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -69,7 +87,10 @@ class MainActivity : ComponentActivity() {
val routeModel = RouteModel() val routeModel = RouteModel()
var tilt = 50.0 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 { val stepData: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>() MutableLiveData<StepData>()
} }
@@ -80,15 +101,23 @@ class MainActivity : ComponentActivity() {
val observer = Observer<String> { newRoute -> val observer = Observer<String> { newRoute ->
if (newRoute.isNotEmpty()) { if (newRoute.isNotEmpty()) {
routeModel.startNavigation(newRoute, applicationContext) routeModel.startNavigation(newRoute, applicationContext)
routeData.value = routeModel.route.routeGeoJson routeData.value = routeModel.curRoute.routeGeoJson
simulate() if (useMock) {
//test() when (type) {
1 -> simulate()
2 -> test()
3 -> gpx(
context = applicationContext
)
4 -> testSingle()
}
}
} }
} }
val cameraPosition = MutableLiveData( val cameraPosition = MutableLiveData(
CameraPosition( CameraPosition(
zoom = 15.0, zoom = 15.0, target = Position(latitude = 48.1857475, longitude = 11.5793627)
target = Position(latitude = 48.1857475, longitude = 11.5793627)
) )
) )
@@ -99,30 +128,28 @@ class MainActivity : ComponentActivity() {
private var loadRecentPlaces = false 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]) @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val darkModeSettings = getIntKeyValue(applicationContext, Constants.DARK_MODE_SETTINGS)
baseStyle = BaseStyleModel().readStyle(applicationContext, darkModeSettings, false)
if (useMock) { if (useMock) {
checkMockLocationEnabled() checkMockLocationEnabled()
} }
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.lastLocation fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? ->
.addOnSuccessListener { location : android.location.Location? -> if (useMock) {
if (useMock) { mock = MockLocation(locationManager)
mock = MockLocation(locationManager) mock.setMockLocation(
mock.setMockLocation( homeVogelhart.latitude, homeVogelhart.longitude
location?.latitude ?: homeLocation.latitude, )
location?.longitude ?: homeLocation.longitude navigationViewModel.route.observe(this, observer)
)
}
} }
}
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
CheckPermissionScreen() CheckPermissionScreen()
@@ -140,23 +167,35 @@ class MainActivity : ComponentActivity() {
permissions = permissions, permissions = permissions,
requiredPermissions = listOf(permissions.first()), requiredPermissions = listOf(permissions.first()),
onGranted = { onGranted = {
Content() App()
// auto navigate
if (useMock) {
// navigationViewModel.loadRoute(
// applicationContext,
// homeVogelhart,
// homeHohenwaldeck,
// 0F
// )
}
}, },
) )
} }
@SuppressLint("AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun Content() { fun StartScreen(
navController: NavHostController
) {
val scaffoldState = rememberBottomSheetScaffoldState() val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val sheetPeekHeightState = remember { mutableStateOf(256.dp) } val sheetPeekHeightState = remember { mutableStateOf(256.dp) }
val locationProvider = rememberDefaultLocationProvider( val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds, updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
desiredAccuracy = DesiredAccuracy.Highest
) )
val userLocationState = rememberUserLocationState(locationProvider) val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState() val locationState = locationProvider.location.collectAsState()
@@ -194,77 +233,108 @@ class MainActivity : ComponentActivity() {
applicationContext, applicationContext,
userLocationState, userLocationState,
step, step,
nextStep,
cameraPosition, cameraPosition,
routeData, 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 @Composable
fun SheetContent( fun SheetContent(
locationState: Double, locationState: Double, step: StepData?, nextStep: StepData?, closeSheet: () -> Unit
step: StepData?,
nextStep: StepData?,
closeSheet: () -> Unit
) { ) {
if (!routeModel.isNavigating()) { if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() } SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() }
} else { } else {
NavigationSheet( NavigationSheet(
routeModel, step!!, nextStep!!, applicationContext,
routeModel,
step!!,
nextStep!!,
{ stopNavigation { closeSheet() } }, { stopNavigation { closeSheet() } },
{ simulateNavigation() } { simulateNavigation() })
)
} }
// For recomposition! // For recomposition!
Text("$locationState", fontSize = 12.sp) Text("$locationState", fontSize = 12.sp)
} }
fun updateLocation(location: Location?) { fun updateLocation(location: Location?) {
if (location != null if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) {
&& lastLocation.latitude != location.position.latitude
&& lastLocation.longitude != location.position.longitude
) {
val currentLocation = location(location.position.longitude, location.position.latitude) val currentLocation = location(location.position.longitude, location.position.latitude)
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
with(routeModel) { with(routeModel) {
if (isNavigating()) { if (isNavigating()) {
updateLocation(currentLocation, navigationViewModel) updateLocation(currentLocation, navigationViewModel)
stepData.value = currentStep() stepData.value = currentStep()
if (route.currentStep + 1 <= legs.steps.size) { nextStepData.value = nextStep()
nextStepData.value = nextStep() if (navState.maneuverType in 39..42 && routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) {
} // stopNavigation()
if (routeState.maneuverType == 39 navState.copy(arrived = true)
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
// stopNavigation()
routeState = routeState.copy(arrived = true)
routeData.value = "" routeData.value = ""
} }
} }
} }
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
val zoom = calculateZoom(location.speed) val zoom = calculateZoom(location.speed)
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
zoom = zoom, zoom = zoom, target = location.position, bearing = bearing
target = location.position,
bearing = bearing
), ),
) )
lastLocation = currentLocation lastLocation = currentLocation
if (!loadRecentPlaces) { if (!loadRecentPlaces) {
navigationViewModel.loadRecentPlaces(applicationContext, lastLocation) navigationViewModel.loadRecentPlaces(applicationContext, lastLocation, 0F)
loadRecentPlaces = true loadRecentPlaces = true
} }
} }
} }
fun stopNavigation(closeSheet: () -> Unit) { fun stopNavigation(closeSheet: () -> Unit) {
val latitude = routeModel.curRoute.waypoints[0][1]
val longitude = routeModel.curRoute.waypoints[0][0]
closeSheet() closeSheet()
routeModel.stopNavigation() routeModel.stopNavigation()
if (useMock) {
mock.setMockLocation(latitude, longitude)
}
routeData.value = "" routeData.value = ""
stepData.value = StepData("", 0.0, 0, 0, 0, 0.0) stepData.value = StepData("", 0.0, 0, 0, 0, 0.0)
} }
@@ -276,14 +346,10 @@ class MainActivity : ComponentActivity() {
private fun checkMockLocationEnabled() { private fun checkMockLocationEnabled() {
try { try {
// Check if mock location is enabled for this app // Check if mock location is enabled for this app
val appOpsManager = val appOpsManager = getSystemService(APP_OPS_SERVICE) as AppOpsManager
getSystemService(APP_OPS_SERVICE) as AppOpsManager val mode = appOpsManager.checkOp(
val mode = AppOpsManager.OPSTR_MOCK_LOCATION, Process.myUid(), packageName
appOpsManager.checkOp( )
AppOpsManager.OPSTR_MOCK_LOCATION,
Process.myUid(),
packageName
)
if (mode != AppOpsManager.MODE_ALLOWED) { if (mode != AppOpsManager.MODE_ALLOWED) {
Toast.makeText( Toast.makeText(
@@ -297,13 +363,14 @@ class MainActivity : ComponentActivity() {
} }
} }
fun simulate() { fun simulate() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
for ((index, step) in routeModel.legs.steps.withIndex()) { for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) { if (routeModel.isNavigating()) {
if (routeModel.isNavigating()) { val deviation = 0.0
if (index in 0..routeModel.curRoute.waypoints.size) {
mock.setMockLocation(waypoint[1], waypoint[0]) mock.setMockLocation(waypoint[1], waypoint[0])
delay(800L) // Thread.sleep(500)
} }
} }
} }
@@ -311,15 +378,67 @@ class MainActivity : ComponentActivity() {
} }
fun test() { fun test() {
for ((index, step) in routeModel.legs.steps.withIndex()) { for ((index, step) in routeModel.curLeg.steps.withIndex()) {
println("${step.maneuver.waypoints.size}") //if (index in 3..3) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) { for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
routeModel.updateLocation(location(waypoint[0], waypoint[1]), navigationViewModel) routeModel.updateLocation(
routeModel.currentStep() location(waypoint[0], waypoint[1]), navigationViewModel
if (index + 1 <= routeModel.legs.steps.size) { )
nextStepData.value = routeModel.nextStep() 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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.Alignment
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle 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.MapLibre
import com.kouros.navigation.car.map.NavigationImage 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.StepData
import com.kouros.navigation.data.tomtom.TrafficData
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.LocationTrackingEffect
@@ -32,12 +30,15 @@ fun MapView(
applicationContext: Context, applicationContext: Context,
userLocationState: UserLocationState, userLocationState: UserLocationState,
step: StepData?, step: StepData?,
nextStep: StepData?,
cameraPosition: MutableLiveData<CameraPosition>, cameraPosition: MutableLiveData<CameraPosition>,
routeData: MutableLiveData<String>, 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 width = metrics.bounds.width()
val height = metrics.bounds.height() val height = metrics.bounds.height()
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp) val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
@@ -55,21 +56,20 @@ fun MapView(
zoom = 15.0, zoom = 15.0,
) )
) )
val baseStyle = remember {
mutableStateOf(BaseStyle.Uri(Constants.STYLE)) val rememberBaseStyle = rememberBaseStyle( baseStyle)
}
DarkMode(applicationContext, baseStyle)
Column { Column {
NavigationInfo(step) NavigationInfo(step, nextStep)
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
MapLibre( MapLibre(
applicationContext, applicationContext,
cameraState, cameraState,
baseStyle, rememberBaseStyle,
route, route,
emptyMap(),
ViewStyle.VIEW ViewStyle.VIEW
) )
LocationTrackingEffect( LocationTrackingEffect(
locationState = userLocationState, locationState = userLocationState,
) { ) {
cameraState.animateTo( cameraState.animateTo(

View File

@@ -18,7 +18,7 @@ import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.round import com.kouros.navigation.utils.round
@Composable @Composable
fun NavigationInfo(step: StepData?) { fun NavigationInfo(step: StepData?, nextStep: StepData?) {
if (step != null && step.instruction.isNotEmpty()) { if (step != null && step.instruction.isNotEmpty()) {
Card(modifier = Modifier.padding(top = 60.dp)) { Card(modifier = Modifier.padding(top = 60.dp)) {
Column() { Column() {
@@ -28,6 +28,10 @@ fun NavigationInfo(step: StepData?) {
contentDescription = stringResource(id = R.string.accept_action_title), contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp), modifier = Modifier.size(48.dp, 48.dp),
) )
if (step.currentManeuverType == 46
|| step.currentManeuverType == 45) {
Text(text ="Exit ${step.exitNumber}", fontSize = 20.sp)
}
Column { Column {
if (step.leftStepDistance < 1000) { if (step.leftStepDistance < 1000) {
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp) Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp)
@@ -39,11 +43,13 @@ fun NavigationInfo(step: StepData?) {
} }
Text(text = step.instruction, fontSize = 20.sp) Text(text = step.instruction, fontSize = 20.sp)
} }
Icon( if (nextStep != null && step.icon != nextStep.icon) {
painter = painterResource(step.icon), Icon(
contentDescription = stringResource(id = R.string.accept_action_title), painter = painterResource(nextStep.icon),
modifier = Modifier.size(48.dp, 48.dp), 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.formatDateTime import com.kouros.navigation.utils.formatDateTime
@@ -23,19 +24,26 @@ import com.kouros.navigation.utils.round
@Composable @Composable
fun NavigationSheet( fun NavigationSheet(
applicationContext: Context,
routeModel: RouteModel, routeModel: RouteModel,
step: StepData, step: StepData,
nextStep: StepData, nextStep: StepData,
stopNavigation: () -> Unit, stopNavigation: () -> Unit,
simulateNavigation: () -> 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 { Column {
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) { FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
Text(formatDateTime(step.arrivalTime), fontSize = 22.sp) Text(formatDateTime(step.arrivalTime), fontSize = 22.sp)
Spacer(Modifier.size(30.dp)) Spacer(Modifier.size(30.dp))
Text("$distance km", fontSize = 22.sp) Text("$distance km", fontSize = 22.sp)
} }
HorizontalDivider() HorizontalDivider()
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) { FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
@@ -48,6 +56,15 @@ fun NavigationSheet(
modifier = Modifier.size(24.dp, 24.dp), 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)) Spacer(Modifier.size(30.dp))
if (!routeModel.isNavigating()) { if (!routeModel.isNavigating()) {

View File

@@ -57,38 +57,31 @@ fun SearchSheet(
if (search.value != null) { if (search.value != null) {
searchResults.addAll(search.value!!) 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) { if (recentPlaces.value != null) {
val textFieldState = rememberTextFieldState()
val items = listOf(recentPlaces) val items = listOf(recentPlaces)
if (items.isNotEmpty()) { if (items.isNotEmpty()) {
SearchBar( RecentPlaces(recentPlaces.value!!, viewModel, applicationContext, location, closeSheet)
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location,
closeSheet = { 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 = { Button(onClick = {
val places = viewModel.loadRecentPlace() val places = viewModel.loadRecentPlace()
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude) val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
viewModel.loadRoute(applicationContext, location, toLocation) viewModel.loadRoute(applicationContext, location, toLocation, 0F)
closeSheet() closeSheet()
}) { }) {
Icon( Icon(
@@ -138,15 +131,7 @@ fun SearchBar(
closeSheet: () -> Unit closeSheet: () -> Unit
) { ) {
var expanded by rememberSaveable { mutableStateOf(true) } var expanded by rememberSaveable { mutableStateOf(true) }
Box(
modifier
.fillMaxSize()
.semantics { isTraversalGroup = true }
) {
SearchBar( SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = 0f },
inputField = { inputField = {
SearchBarDefaults.InputField( SearchBarDefaults.InputField(
leadingIcon = { leadingIcon = {
@@ -179,7 +164,6 @@ fun SearchBar(
SearchPlaces(searchResults, viewModel, context, location, closeSheet) SearchPlaces(searchResults, viewModel, context, location, closeSheet)
} }
} }
}
} }
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) { private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
@@ -223,7 +207,7 @@ private fun SearchPlaces(
viewModel.saveRecent(pl) viewModel.saveRecent(pl)
val toLocation = val toLocation =
location(place.lon.toDouble(), place.lat.toDouble()) location(place.lon.toDouble(), place.lat.toDouble())
viewModel.loadRoute(context, location, toLocation) viewModel.loadRoute(context, location, toLocation, 0F)
closeSheet() closeSheet()
} }
.fillMaxWidth() .fillMaxWidth()
@@ -261,7 +245,7 @@ private fun RecentPlaces(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
val toLocation = location(place.longitude, place.latitude) val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation) viewModel.loadRoute(context, location, toLocation, 0F)
closeSheet() closeSheet()
} }
.fillMaxWidth() .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 import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) //val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC) //val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8) //val Pink80 = Color(0xFFEFB8C8)
//
//val Purple40 = Color(0xFF6650a4)
//val PurpleGrey40 = Color(0xFF625b71)
//val Pink40 = Color(0xFF7D5260)
val Purple40 = Color(0xFF6650a4) val md_theme_light_primary = Color(0xFF825500)
val PurpleGrey40 = Color(0xFF625b71) val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val Pink40 = Color(0xFF7D5260) 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 package com.kouros.navigation.ui.theme
import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable 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.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme( private val LightColors = lightColorScheme(
primary = Purple80, primary = md_theme_light_primary,
secondary = PurpleGrey80, onPrimary = md_theme_light_onPrimary,
tertiary = Pink80 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 private val DarkColors = darkColorScheme(
background = Color(0xFFFFFBFE), primary = md_theme_dark_primary,
surface = Color(0xFFFFFBFE), onPrimary = md_theme_dark_onPrimary,
onPrimary = Color.White, primaryContainer = md_theme_dark_primaryContainer,
onSecondary = Color.White, onPrimaryContainer = md_theme_dark_onPrimaryContainer,
onTertiary = Color.White, secondary = md_theme_dark_secondary,
onBackground = Color(0xFF1C1B1F), onSecondary = md_theme_dark_onSecondary,
onSurface = Color(0xFF1C1B1F), 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 @Composable
fun NavigationTheme( fun NavigationTheme(
darkTheme: Boolean = isSystemInDarkTheme(), useDarkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { val context = LocalContext.current
dynamicColor -> { val colors = run {
val context = LocalContext.current if (useDarkTheme) dynamicDarkColorScheme(context)
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
} }
MaterialTheme( val view = LocalView.current
colorScheme = colorScheme, if (!view.isInEditMode) {
typography = Typography, SideEffect {
content = content 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 import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with // 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( bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp letterSpacing = 0.15.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
), ),
labelSmall = TextStyle( bodyMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium, 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, lineHeight = 16.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
) )
*/ )
)

View File

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

View File

@@ -46,6 +46,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
implementation(libs.androidx.app.projected)
//implementation(libs.maplibre.composeMaterial3) //implementation(libs.maplibre.composeMaterial3)
implementation(project(":common:data")) 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_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" /> <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 <meta-data
android:name="androidx.car.app.minCarApiLevel" android:name="androidx.car.app.minCarApiLevel"
android:value="1" /> 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.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.util.Log import android.util.Log
@@ -12,6 +13,13 @@ import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.Session 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.location.LocationListenerCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver 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.NavigationScreen
import com.kouros.navigation.car.screen.RequestPermissionScreen import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen 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_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.ROUTE_ENGINE
import com.kouros.navigation.data.Constants.TAG 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.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine import com.kouros.navigation.utils.NavigationUtils.getViewModel
class NavigationSession : Session(), NavigationScreen.Listener { class NavigationSession : Session(), NavigationScreen.Listener {
@@ -45,27 +54,33 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lateinit var surfaceRenderer: SurfaceRenderer lateinit var surfaceRenderer: SurfaceRenderer
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? -> var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
updateLocation(location!!) val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
if (!useCarLocation) {
updateLocation(location!!)
}
} }
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
Log.i(TAG, "In onCreate()")
} }
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
Log.i(TAG, "In onResume()")
} }
override fun onPause(owner: LifecycleOwner) { override fun onPause(owner: LifecycleOwner) {
Log.i(TAG, "In onPause()")
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
Log.i(TAG, "In onStop()")
} }
override fun onDestroy(owner: LifecycleOwner) { 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()") Log.i(TAG, "In onDestroy()")
val locationManager = val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as 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 { init {
val lifecycle: Lifecycle = lifecycle val lifecycle: Lifecycle = lifecycle
lifecycle.addObserver(mLifeCycleObserver) 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 { override fun onCreateScreen(intent: Intent): Screen {
navigationViewModel = getRouteEngine(carContext) navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
routeModel = RouteCarModel() routeModel = RouteCarModel()
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel) 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) 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) || (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED) == PackageManager.PERMISSION_GRANTED)
) { ) {
requestLocationUpdates() requestLocationUpdates()
} else { } else {
@@ -111,9 +167,29 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
) )
} }
addSensors()
return navigationScreen 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) { override fun onNewIntent(intent: Intent) {
val screenManager = carContext.getCarService(ScreenManager::class.java) val screenManager = carContext.getCarService(ScreenManager::class.java)
if ((CarContext.ACTION_NAVIGATE == intent.action)) { if ((CarContext.ACTION_NAVIGATE == intent.action)) {
@@ -124,8 +200,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
SearchScreen( SearchScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer,
location, navigationViewModel
navigationViewModel,
// TODO: Uri // TODO: Uri
) )
) { obj: Any? -> ) { 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") @SuppressLint("MissingPermission")
fun requestLocationUpdates() { fun requestLocationUpdates() {
val locationManager = val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (location != null) { if (location != null) {
navigationViewModel.loadRecentPlace(location = location, surfaceRenderer.carOrientation, carContext)
updateLocation(location) updateLocation(location)
locationManager.requestLocationUpdates( locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 1000, /* minTimeMs= */ 500,
/* minDistanceM= */ 5f, /* minDistanceM= */ 5f,
mLocationListener mLocationListener
) )
@@ -169,17 +250,19 @@ class NavigationSession : Session(), NavigationScreen.Listener {
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
if (routeModel.isNavigating()) { 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) navigationScreen.updateTrip(location)
if (distance < MAXIMAL_SNAP_CORRECTION) { if (!routeModel.navState.arrived) {
surfaceRenderer.updateLocation(snapedLocation) val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
} else { val distance = location.distanceTo(snapedLocation)
surfaceRenderer.updateLocation(location) if (distance > MAXIMAL_ROUTE_DEVIATION) {
navigationScreen.calculateNewRoute(routeModel.navState.destination)
return
}
if (distance < MAXIMAL_SNAP_CORRECTION) {
surfaceRenderer.updateLocation(snapedLocation)
} else {
surfaceRenderer.updateLocation(location)
}
} }
} else { } else {
surfaceRenderer.updateLocation(location) surfaceRenderer.updateLocation(location)

View File

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

View File

@@ -1,9 +1,8 @@
package com.kouros.navigation.car.map package com.kouros.navigation.car.map
import android.location.Location
import android.content.Context import android.content.Context
import android.location.Location
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize 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.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable 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.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.NavigationColor
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor 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.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.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.ast.Expression
import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.const
import org.maplibre.compose.expressions.dsl.exponential import org.maplibre.compose.expressions.dsl.exponential
import org.maplibre.compose.expressions.dsl.image import org.maplibre.compose.expressions.dsl.image
import org.maplibre.compose.expressions.dsl.interpolate import org.maplibre.compose.expressions.dsl.interpolate
import org.maplibre.compose.expressions.dsl.zoom import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.expressions.value.ColorValue
import org.maplibre.compose.layers.Anchor import org.maplibre.compose.layers.Anchor
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
import org.maplibre.compose.layers.LineLayer import org.maplibre.compose.layers.LineLayer
@@ -87,8 +88,9 @@ fun cameraState(
fun MapLibre( fun MapLibre(
context: Context, context: Context,
cameraState: CameraState, cameraState: CameraState,
baseStyle: MutableState<BaseStyle.Uri>, baseStyle: BaseStyle.Json,
route: String?, route: String?,
traffic: Map<String, String>?,
viewStyle: ViewStyle, viewStyle: ViewStyle,
speedCameras: String? = "" speedCameras: String? = ""
) { ) {
@@ -98,7 +100,7 @@ fun MapLibre(
OrnamentOptions(isScaleBarEnabled = false) OrnamentOptions(isScaleBarEnabled = false)
), ),
cameraState = cameraState, cameraState = cameraState,
baseStyle = baseStyle.value baseStyle = baseStyle
) { ) {
getBaseSource(id = "openmaptiles")?.let { tiles -> getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) { if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
@@ -107,7 +109,8 @@ fun MapLibre(
if (viewStyle == ViewStyle.AMENITY_VIEW) { if (viewStyle == ViewStyle.AMENITY_VIEW) {
AmenityLayer(route) AmenityLayer(route)
} else { } else {
RouteLayer(route) RouteLayer(route, traffic!!)
//RouteLayerPoint(route )
} }
SpeedCameraLayer(speedCameras) SpeedCameraLayer(speedCameras)
} }
@@ -115,8 +118,9 @@ fun MapLibre(
//Puck(cameraState, lastLocation) //Puck(cameraState, lastLocation)
} }
} }
@Composable @Composable
fun RouteLayer(routeData: String?) { fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
if (routeData != null && routeData.isNotEmpty()) { if (routeData != null && routeData.isNotEmpty()) {
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
LineLayer( 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 @Composable
fun AmenityLayer(routeData: String?) { fun AmenityLayer(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) { if (routeData != null && routeData.isNotEmpty()) {
val color = if (routeData.contains(Constants.PHARMACY)) { var color = const(Color.Red)
const(Color.Red) var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
} else { if (routeData.contains(Constants.CHARGING_STATION)) {
const(Color.Green) 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)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
SymbolLayer( SymbolLayer(
id = "amenity-layer", id = "amenity-layer",
source = routes, source = routes,
iconImage = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true), iconImage = img,
iconColor = color, 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 @Composable
fun SpeedCameraLayer(speedCameras: String?) { fun SpeedCameraLayer(speedCameras: String?) {
if (speedCameras != null && speedCameras.isNotEmpty()) { if (speedCameras != null && speedCameras.isNotEmpty()) {
val color = const(Color.DarkGray) val color = const(Color.Red)
val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras)) val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras))
SymbolLayer( SymbolLayer(
id = "speed-camera-layer", id = "speed-camera-layer",
source = cameraSource, source = cameraSource,
iconImage = image(painterResource(R.drawable.speed_camera_48px), drawAsSdf = true), iconImage = image(painterResource(R.drawable.speed_camera_24px), drawAsSdf = true),
iconColor = color, iconColor = color,
iconSize = iconSize =
interpolate( interpolate(
type = exponential(1.2f), type = exponential(1.2f),
input = zoom(), input = zoom(),
5 to const(0.4f), 5 to const(0.7f),
6 to const(0.7f), 6 to const(1.0f),
7 to const(1.75f), 7 to const(2.0f),
20 to const(3f), 20 to const(4f),
), ),
) )
} }
} }
@Composable @Composable
fun BuildingLayer(tiles: Source) { fun BuildingLayer(tiles: Source) {
Anchor.Replace("building-3d") { Anchor.Replace("building-3d") {
@@ -218,6 +305,7 @@ fun DrawNavigationImages(
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) { if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
MaxSpeed(width, height, maxSpeed) MaxSpeed(width, height, maxSpeed)
} }
//DebugInfo(width, height, routeModel)
} }
@Composable @Composable
@@ -251,7 +339,7 @@ private fun CurrentSpeed(
curSpeed: Float, curSpeed: Float,
maxSpeed: Int maxSpeed: Int
) { ) {
val radius = 32 val radius = 34
Box( Box(
modifier = Modifier modifier = Modifier
.padding( .padding(
@@ -317,7 +405,7 @@ private fun MaxSpeed(
height: Int, height: Int,
maxSpeed: Int, maxSpeed: Int,
) { ) {
val radius = 20 val radius = 24
Box( Box(
modifier = Modifier modifier = Modifier
.padding( .padding(
@@ -367,25 +455,59 @@ private fun MaxSpeed(
} }
@Composable @Composable
fun DarkMode(context: Context, baseStyle: MutableState<BaseStyle.Uri>) { fun DebugInfo(
val darkMode = getIntKeyValue(context, Constants.DARK_MODE_SETTINGS) width: Int,
if (darkMode == 0) { height: Int,
baseStyle.value = BaseStyle.Uri(Constants.STYLE) 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)
} @Composable
if (darkMode == 2) { fun rememberBaseStyle(baseStyle: BaseStyle.Json): BaseStyle.Json {
baseStyle.value = val rememberBaseStyle by remember() {
(if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri( mutableStateOf(baseStyle)
Constants.STYLE
))
} }
return rememberBaseStyle
} }
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues { fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) { return when (viewStyle) {
ViewStyle.VIEW -> PaddingValues(start = 50.dp, top = distanceFromTop(height).dp) ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(
start = 50.dp,
top = distanceFromTop(height).dp
)
ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp) ViewStyle.PREVIEW -> PaddingValues(start = 150.dp, bottom = 0.dp)
else -> PaddingValues(start = 250.dp, bottom = 0.dp) else -> PaddingValues(start = 250.dp, bottom = 0.dp)
} }

View File

@@ -15,6 +15,7 @@
*/ */
package com.kouros.navigation.car.navigation package com.kouros.navigation.car.navigation
import android.location.Location
import android.text.SpannableString import android.text.SpannableString
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes 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.CarText
import androidx.car.app.model.DateTimeWithZone import androidx.car.app.model.DateTimeWithZone
import androidx.car.app.model.Distance 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
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.Step
import androidx.car.app.navigation.model.TravelEstimate import androidx.car.app.navigation.model.TravelEstimate
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel 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.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -46,46 +55,61 @@ class RouteCarModel() : RouteModel() {
val stepData = currentStep() val stepData = currentStep()
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(stepData.instruction) 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 = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
.setManeuver( .setManeuver(
Maneuver.Builder(stepData.maneuverType) maneuver.build()
.setIcon(createCarIcon(carContext, stepData.icon))
.build()
) )
.setRoad(routeState.destination.street!!) if (navState.destination.street != null) {
.build() step.setRoad(navState.destination.street!!)
return step }
if (stepData.lane.isNotEmpty()) {
addLanes(carContext, step, stepData)
}
return step.build()
} }
/** Returns the next [Step] with information such as the cue text and images. */ /** 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 stepData = nextStep()
val currentStepCueWithImage: SpannableString = val currentStepCueWithImage: SpannableString =
createString(stepData.instruction) 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 = val step =
Step.Builder(currentStepCueWithImage) Step.Builder(currentStepCueWithImage)
.setManeuver( .setManeuver(
Maneuver.Builder(stepData.maneuverType) maneuver.build()
.setIcon(createCarIcon(carContext, stepData.icon))
.build()
) )
.build() .build()
return step return step
} }
fun travelEstimate(carContext: CarContext): TravelEstimate { fun travelEstimate(carContext: CarContext): TravelEstimate {
val timeLeft = travelLeftTime() val timeLeft = routeCalculator.travelLeftTime()
val timeToDestinationMillis = val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong()) TimeUnit.SECONDS.toMillis(timeLeft.toLong())
val leftDistance = travelLeftDistance() val leftDistance = routeCalculator.travelLeftDistance() / 1000
val displayUnit = if (leftDistance > 1.0) { val displayUnit = if (leftDistance > 1.0) {
Distance.UNIT_KILOMETERS Distance.UNIT_KILOMETERS
} else { } else {
Distance.UNIT_METERS Distance.UNIT_METERS
} }
val arivalTime = DateTimeWithZone.create( val arrivalTime = DateTimeWithZone.create(
arrivalTime(), routeCalculator.arrivalTime(),
TimeZone.getTimeZone("Europe/Berlin") TimeZone.getTimeZone("Europe/Berlin")
) )
val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination. val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination.
@@ -93,23 +117,52 @@ class RouteCarModel() : RouteModel() {
leftDistance, leftDistance,
displayUnit displayUnit
), // Arrival time at the destination with the destination time zone. ), // Arrival time at the destination with the destination time zone.
arivalTime arrivalTime
) )
.setRemainingTimeSeconds( .setRemainingTimeSeconds(
TimeUnit.MILLISECONDS.toSeconds( TimeUnit.MILLISECONDS.toSeconds(
timeToDestinationMillis timeToDestinationMillis
) )
) )
.setRemainingTimeColor(CarColor.YELLOW) .setRemainingTimeColor(CarColor.GREEN)
.setRemainingDistanceColor(CarColor.RED) .setRemainingDistanceColor(CarColor.BLUE)
if (routeState.travelMessage.isNotEmpty()) { if (navState.travelMessage.isNotEmpty()) {
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px)) travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
travelBuilder.setTripText(CarText.create(routeState.travelMessage)) travelBuilder.setTripText(CarText.create(navState.travelMessage))
} }
return travelBuilder.build() 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( fun createString(
text: String text: String
): SpannableString { ): SpannableString {
@@ -125,23 +178,35 @@ class RouteCarModel() : RouteModel() {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build() return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
} }
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String?) { fun createCarIcon(iconCompat: IconCompat): CarIcon {
carContext.getCarService<AppManager?>(AppManager::class.java) return CarIcon.Builder(iconCompat).build()
.showAlert(createAlert(carContext, distance, maxSpeed))
} }
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 title = createCarText(carContext, R.string.speed_camera)
val subtitle = CarText.create(maxSpeed!!) val subtitle = CarText.create(maxSpeed!!)
val icon = CarIcon.ALERT
val dismissAction: Action = createToastAction( val dismissAction: Action = createToastAction(
carContext, carContext,
R.string.exit_action_title, R.string.exit_action_title, R.string.exit_action_title, R.string.exit_action_title,
FLAG_DEFAULT FLAG_DEFAULT
) )
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */10000)
.setSubtitle(subtitle) .setSubtitle(subtitle)
.setIcon(icon) .setIcon(icon)
.addAction(dismissAction).setCallback(object : AlertCallback { .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.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CHARGING_STATION import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION import com.kouros.navigation.data.Constants.FUEL_STATION
@@ -23,8 +24,7 @@ import com.kouros.navigation.model.ViewModel
class CategoriesScreen( class CategoriesScreen(
private val carContext: CarContext, private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer, private val surfaceRenderer: SurfaceRenderer,
private val location: Location, private val viewModel: ViewModel,
private val viewModel: ViewModel
) : Screen(carContext) { ) : Screen(carContext) {
var categories: List<Category> = listOf( var categories: List<Category> = listOf(
@@ -47,7 +47,6 @@ class CategoriesScreen(
CategoryScreen( CategoryScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer,
location,
it.id, it.id,
viewModel viewModel
) )

View File

@@ -19,8 +19,9 @@ import androidx.lifecycle.Observer
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage import com.kouros.navigation.car.navigation.NavigationMessage
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants 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.data.overpass.Elements
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils.createPointCollection import com.kouros.navigation.utils.GeoUtils.createPointCollection
@@ -31,9 +32,8 @@ import kotlin.math.min
class CategoryScreen( class CategoryScreen(
private val carContext: CarContext, private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer, private val surfaceRenderer: SurfaceRenderer,
location: Location,
private val category: String, private val category: String,
private val viewModel: ViewModel private val viewModel: ViewModel,
) : Screen(carContext) { ) : Screen(carContext) {
var elements = listOf<Elements>() var elements = listOf<Elements>()
@@ -44,10 +44,10 @@ class CategoryScreen(
val loc = location(0.0, 0.0) val loc = location(0.0, 0.0)
elements.forEach { elements.forEach {
if (loc.latitude == 0.0) { if (loc.latitude == 0.0) {
loc.longitude = it.lon!! loc.longitude = it.lon
loc.latitude = it.lat!! loc.latitude = it.lat
} }
coordinates.add(listOf(it.lon!!, it.lat!!)) coordinates.add(listOf(it.lon, it.lat))
} }
if (elements.isNotEmpty()) { if (elements.isNotEmpty()) {
val route = createPointCollection(coordinates, category) val route = createPointCollection(coordinates, category)
@@ -58,7 +58,7 @@ class CategoryScreen(
init { init {
viewModel.elements.observe(this, observer) viewModel.elements.observe(this, observer)
viewModel.getAmenities(category, location) viewModel.getAmenities(category, surfaceRenderer.lastLocation)
} }
@@ -111,7 +111,7 @@ class CategoryScreen(
} }
val row = Row.Builder() val row = Row.Builder()
.setOnClickListener { .setOnClickListener {
val location = location(it.lon!!, it.lat!!) val location = location(it.lon, it.lat)
surfaceRenderer.setCategoryLocation(location, category) surfaceRenderer.setCategoryLocation(location, category)
} }
.setTitle(name) .setTitle(name)
@@ -126,6 +126,28 @@ class CategoryScreen(
} else { } else {
row.addText(carText("${it.tags.openingHours}")) 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() 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.ActionStrip
import androidx.car.app.model.CarColor import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.model.Header import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate 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.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class NavigationScreen( class NavigationScreen(
@@ -58,6 +57,7 @@ class NavigationScreen(
var recentPlace = Place() var recentPlace = Place()
var navigationType = NavigationType.VIEW var navigationType = NavigationType.VIEW
var lastTrafficDate = LocalDateTime.of(1960, 6, 21, 0, 0)
val observer = Observer<String> { route -> val observer = Observer<String> { route ->
if (route.isNotEmpty()) { if (route.isNotEmpty()) {
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
@@ -74,6 +74,10 @@ class NavigationScreen(
invalidate() invalidate()
} }
} }
val trafficObserver = Observer<Map<String, String> > { traffic ->
surfaceRenderer.setTrafficData(traffic)
invalidate()
}
val placeObserver = Observer<SearchResult> { searchResult -> val placeObserver = Observer<SearchResult> { searchResult ->
val place = Place( val place = Place(
@@ -93,22 +97,19 @@ class NavigationScreen(
var speedCameras = listOf<Elements>() var speedCameras = listOf<Elements>()
val speedObserver = Observer<List<Elements>> { cameras -> val speedObserver = Observer<List<Elements>> { cameras ->
speedCameras = cameras speedCameras = cameras
val coordinates = mutableListOf<List<Double>>() val coordinates = mutableListOf<List<Double>>()
val loc = location(0.0, 0.0)
cameras.forEach { cameras.forEach {
val loc =
location(longitude = it.lon!!, latitude = it.lat!!)
coordinates.add(listOf(it.lon!!, it.lat!!)) coordinates.add(listOf(it.lon!!, it.lat!!))
} }
val speedData = GeoUtils.createPointCollection(coordinates, "radar") val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCamerasData.value =speedData surfaceRenderer.speedCamerasData.value = speedData
} }
init { init {
viewModel.route.observe(this, observer) viewModel.route.observe(this, observer)
viewModel.traffic.observe(this, trafficObserver);
viewModel.recentPlace.observe(this, recentObserver) viewModel.recentPlace.observe(this, recentObserver)
viewModel.loadRecentPlace(location = surfaceRenderer.lastLocation)
viewModel.placeLocation.observe(this, placeObserver) viewModel.placeLocation.observe(this, placeObserver)
viewModel.speedCameras.observe(this, speedObserver) viewModel.speedCameras.observe(this, speedObserver)
} }
@@ -149,11 +150,11 @@ class NavigationScreen(
} }
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.routeState.arrived) { if (routeModel.navState.arrived) {
val timer = object : CountDownTimer(8000, 1000) { val timer = object : CountDownTimer(8000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
routeModel.routeState = routeModel.routeState.copy(arrived = false) routeModel.navState = routeModel.navState.copy(arrived = false)
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
invalidate() invalidate()
} }
@@ -172,8 +173,8 @@ class NavigationScreen(
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = "" var street = ""
if (routeModel.routeState.destination.street != null) { if (routeModel.navState.destination.street != null) {
street = routeModel.routeState.destination.street!! street = routeModel.navState.destination.street!!
} }
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
@@ -232,7 +233,7 @@ class NavigationScreen(
} }
fun getRoutingInfo(): RoutingInfo { fun getRoutingInfo(): RoutingInfo {
var currentDistance = routeModel.leftStepDistance() var currentDistance = routeModel.routeCalculator.leftStepDistance()
val displayUnit = if (currentDistance > 1000.0) { val displayUnit = if (currentDistance > 1000.0) {
currentDistance /= 1000.0 currentDistance /= 1000.0
Distance.UNIT_KILOMETERS Distance.UNIT_KILOMETERS
@@ -240,22 +241,13 @@ class NavigationScreen(
Distance.UNIT_METERS Distance.UNIT_METERS
} }
val nextStep = routeModel.nextStep(carContext = carContext) val nextStep = routeModel.nextStep(carContext = carContext)
if (nextStep != null) { return RoutingInfo.Builder()
return RoutingInfo.Builder() .setCurrentStep(
.setCurrentStep( routeModel.currentStep(carContext = carContext),
routeModel.currentStep(carContext = carContext), Distance.create(currentDistance, displayUnit)
Distance.create(currentDistance, displayUnit) )
) .setNextStep(nextStep)
.setNextStep(nextStep) .build()
.build()
} else {
return RoutingInfo.Builder()
.setCurrentStep(
routeModel.currentStep(carContext = carContext),
Distance.create(currentDistance, displayUnit)
)
.build()
}
} }
private fun createActionStripBuilder(): ActionStrip.Builder { private fun createActionStripBuilder(): ActionStrip.Builder {
@@ -314,8 +306,13 @@ class NavigationScreen(
) )
.setOnClickListener { .setOnClickListener {
val navigateTo = location(recentPlace.longitude, recentPlace.latitude) val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo) viewModel.loadRoute(
routeModel.routeState = routeModel.routeState.copy(destination = recentPlace) carContext,
surfaceRenderer.lastLocation,
navigateTo,
surfaceRenderer.carOrientation
)
routeModel.navState = routeModel.navState.copy(destination = recentPlace)
} }
.build() .build()
} }
@@ -352,7 +349,7 @@ class NavigationScreen(
return Action.Builder() return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px)) .setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
.setOnClickListener { .setOnClickListener {
screenManager.push(SettingsScreen(carContext)) screenManager.push(SettingsScreen(carContext, viewModel))
} }
.build() .build()
} }
@@ -369,6 +366,7 @@ class NavigationScreen(
.build() .build()
).setOnClickListener { ).setOnClickListener {
surfaceRenderer.handleScale(1) surfaceRenderer.handleScale(1)
invalidate()
} }
.build() .build()
} }
@@ -385,6 +383,7 @@ class NavigationScreen(
.build() .build()
).setOnClickListener { ).setOnClickListener {
surfaceRenderer.handleScale(-1) surfaceRenderer.handleScale(-1)
invalidate()
} }
.build() .build()
} }
@@ -401,6 +400,7 @@ class NavigationScreen(
.build() .build()
).setOnClickListener { ).setOnClickListener {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
} }
.build() .build()
} }
@@ -408,7 +408,11 @@ class NavigationScreen(
private fun startSearchScreen() { private fun startSearchScreen() {
screenManager screenManager
.pushForResult( .pushForResult(
SearchScreen(carContext, surfaceRenderer, surfaceRenderer.lastLocation, viewModel) SearchScreen(
carContext,
surfaceRenderer,
viewModel
)
) { obj: Any? -> ) { obj: Any? ->
if (obj != null) { if (obj != null) {
val place = obj as Place val place = obj as Place
@@ -430,8 +434,13 @@ class NavigationScreen(
val location = location(place.longitude, place.latitude) val location = location(place.longitude, place.latitude)
viewModel.saveRecent(place) viewModel.saveRecent(place)
currentNavigationLocation = location currentNavigationLocation = location
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location) viewModel.loadRoute(
routeModel.routeState = routeModel.routeState.copy(destination = place) carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
routeModel.navState = routeModel.navState.copy(destination = place)
invalidate() invalidate()
} }
@@ -449,7 +458,7 @@ class NavigationScreen(
invalidate() invalidate()
val mainThreadHandler = Handler(carContext.mainLooper) val mainThreadHandler = Handler(carContext.mainLooper)
mainThreadHandler.post { mainThreadHandler.post {
object : CountDownTimer(3000, 1000) { object : CountDownTimer(2000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
@@ -461,18 +470,32 @@ class NavigationScreen(
fun reRoute(destination: Place) { fun reRoute(destination: Place) {
val dest = location(destination.longitude, destination.latitude) 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) { 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) updateSpeedCamera(location)
with(routeModel) { with(routeModel) {
updateLocation(location, viewModel) updateLocation(location, viewModel)
if (routeState.maneuverType == Maneuver.TYPE_DESTINATION if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE || navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) { ) {
stopNavigation() stopNavigation()
routeState = routeState.copy(arrived = true) navState = navState.copy(arrived = true)
surfaceRenderer.routeData.value = "" surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL navigationType = NavigationType.ARRIVAL
invalidate() invalidate()
@@ -496,20 +519,27 @@ class NavigationScreen(
val updatedCameras = mutableListOf<Elements>() val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach { speedCameras.forEach {
val plLocation = val plLocation =
location(longitude = it.lon!!, latitude = it.lat!!) location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location) val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble() it.distance = distance.toDouble()
updatedCameras.add(it) updatedCameras.add(it)
} }
val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.first() val camera = sortedList.first()
val bearingSpeedCamera = location.bearingTo(location(camera.lon!!, camera.lat!!))
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
if (camera.distance < 80 try {
&& (bearingSpeedCamera.absoluteValue - bearingRoute.absoluteValue).absoluteValue < 15.0 camera.tags.direction!!.toFloat()
) { } catch ( e: Exception) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed) 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.data.R
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY 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.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue 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 motorWayToggleState = false
private var tollWayToggleState = false private var tollWayToggleState = false
private var carLocationToggleState = false
init { init {
motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY) motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY) tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
carLocationToggleState = getBooleanKeyValue(carContext, CAR_LOCATION)
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -53,6 +62,29 @@ class NavigationSettings(private val carContext: CarContext) : Screen(carContext
}.setChecked(tollWayToggleState).build() }.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle)) 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() return ListTemplate.Builder()
.setSingleList(listBuilder.build()) .setSingleList(listBuilder.build())
.setHeader( .setHeader(
@@ -70,4 +102,12 @@ class NavigationSettings(private val carContext: CarContext) : Screen(carContext
.setToggle(toggle) .setToggle(toggle)
.build() .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.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
@@ -22,21 +21,16 @@ import androidx.lifecycle.Observer
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel 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.CONTACTS
import com.kouros.navigation.data.Constants.FAVORITES import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT 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.data.Place
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import kotlin.math.min
class PlaceListScreen( class PlaceListScreen(
private val carContext: CarContext, private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer, private val surfaceRenderer: SurfaceRenderer,
private val location: Location,
private val category: String, private val category: String,
private val viewModel: ViewModel private val viewModel: ViewModel
) : Screen(carContext) { ) : Screen(carContext) {
@@ -68,13 +62,21 @@ class PlaceListScreen(
fun loadPlaces() { fun loadPlaces() {
if (category == RECENT) { if (category == RECENT) {
viewModel.loadRecentPlaces(carContext, location) viewModel.loadRecentPlaces(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
} }
if (category == CONTACTS) { if (category == CONTACTS) {
viewModel.loadContacts(carContext) viewModel.loadContacts(carContext)
} }
if (category == FAVORITES) { if (category == FAVORITES) {
viewModel.loadFavorites(carContext, location) viewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
} }
} }
@@ -82,9 +84,14 @@ class PlaceListScreen(
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
.setNoItemsMessage(carContext.getString(R.string.no_places)) .setNoItemsMessage(carContext.getString(R.string.no_places))
places.forEach { places.forEach {
val street = if (it.street != null) {
it.street
} else {
""
}
val row = Row.Builder() val row = Row.Builder()
.setImage(contactIcon(it.avatar, it.category)) .setImage(contactIcon(it.avatar, it.category))
.setTitle("${it.street!!} ${it.city}") .setTitle("$street ${it.city}")
.setOnClickListener { .setOnClickListener {
val place = Place( val place = Place(
0, 0,
@@ -117,7 +124,7 @@ class PlaceListScreen(
setSpan( setSpan(
DistanceSpan.create( DistanceSpan.create(
Distance.create( Distance.create(
it.distance.toDouble(), (it.distance/1000).toDouble(),
Distance.UNIT_KILOMETERS Distance.UNIT_KILOMETERS
) )
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE ), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE

View File

@@ -26,9 +26,10 @@ class RequestPermissionScreen(
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val permissions: MutableList<String?> = ArrayList() val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION) permissions.add(permission.ACCESS_FINE_LOCATION)
permissions.add("com.google.android.gms.permission.CAR_SPEED")
//permissions.add(permission.READ_CONTACTS) //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 { val listener: OnClickListener = ParkedOnlyOnClickListener.create {
carContext.requestPermissions( carContext.requestPermissions(

View File

@@ -6,11 +6,10 @@ import androidx.annotation.DrawableRes
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT 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.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
import androidx.car.app.model.DurationSpan import androidx.car.app.model.DurationSpan
@@ -18,19 +17,17 @@ import androidx.car.app.model.Header
import androidx.car.app.model.ItemList import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate import androidx.car.app.model.ListTemplate
import androidx.car.app.model.MessageTemplate import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.OnClickListener
import androidx.car.app.model.Row import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage import com.kouros.navigation.car.navigation.NavigationMessage
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -61,7 +58,12 @@ class RoutePreviewScreen(
init { init {
viewModel.previewRoute.observe(this, observer) viewModel.previewRoute.observe(this, observer)
val location = location(destination.longitude, destination.latitude) 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 { override fun onGetTemplate(): Template {
@@ -74,13 +76,17 @@ class RoutePreviewScreen(
.setFlags(FLAG_DEFAULT) .setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon) .setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate() } .setOnClickListener { this.onNavigate() }
.build() .build()
val itemListBuilder = ItemList.Builder()
var i = 0
routeModel.route.routes.forEach { it ->
itemListBuilder.addItem(createRow(i++, navigateAction))
}
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.route_preview)) .setTitle(carContext.getString(R.string.route_preview))
//.addEndHeaderAction(navigateAction)
.addEndHeaderAction( .addEndHeaderAction(
favoriteAction() favoriteAction()
) )
@@ -89,30 +95,40 @@ class RoutePreviewScreen(
) )
.build() .build()
val message = if (routeModel.isNavigating() && routeModel.route.waypoints!!.isNotEmpty()) { val message =
createRouteText() if (routeModel.isNavigating() && routeModel.curRoute.waypoints!!.isNotEmpty()) {
} else { createRouteText(0)
CarText.Builder("Wait") } else {
.build() 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()
} }
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() return MapWithContentTemplate.Builder()
.setContentTemplate(messageTemplate) .setContentTemplate(content)
.setMapController( .setMapController(
MapController.Builder().setMapActionStrip( MapController.Builder().setMapActionStrip(
getMapActionStrip() getMapActionStrip()
@@ -172,9 +188,13 @@ class RoutePreviewScreen(
) )
.build() .build()
private fun createRouteText(): CarText { private fun createRouteText(index: Int): CarText {
val time = routeModel.route.summary!!.duration val time = routeModel.route.routes[index].summary.duration
val length = BigDecimal(routeModel.route.summary!!.distance).setScale(1, RoundingMode.HALF_EVEN) val length =
BigDecimal(routeModel.route.routes[index].summary.distance).setScale(
1,
RoundingMode.HALF_EVEN
)
val firstRoute = SpannableString(" \u00b7 $length km") val firstRoute = SpannableString(" \u00b7 $length km")
firstRoute.setSpan( firstRoute.setSpan(
DurationSpan.create(time.toLong()), 0, 1, 0 DurationSpan.create(time.toLong()), 0, 1, 0
@@ -183,14 +203,27 @@ class RoutePreviewScreen(
.build() .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() { private fun onNavigate() {
setResult(destination) setResult(destination)
finish() finish()
} }
private fun onRouteSelected(index: Int) { private fun onRouteSelected(index: Int) {
setResult(destination) routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
finish() surfaceRenderer.setPreviewRouteData(routeModel)
//setResult(destination)
//finish()
} }
fun getMapActionStrip(): ActionStrip { 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.annotation.SuppressLint
import android.location.Location import android.location.Location
import android.net.Uri
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
@@ -17,9 +16,9 @@ import androidx.lifecycle.Observer
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
@@ -28,15 +27,14 @@ import com.kouros.navigation.model.ViewModel
class SearchScreen( class SearchScreen(
carContext: CarContext, carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var location: Location, private val viewModel: ViewModel,
private val viewModel: ViewModel
) : Screen(carContext) { ) : Screen(carContext) {
var isSearchComplete: Boolean = false var isSearchComplete: Boolean = false
var categories: List<Category> = listOf( var categories: List<Category> = listOf(
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)), Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)), // Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)), Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)),
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites)) Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites))
) )
@@ -73,7 +71,6 @@ class SearchScreen(
CategoriesScreen( CategoriesScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer,
location,
viewModel viewModel
) )
) { obj: Any? -> ) { obj: Any? ->
@@ -89,7 +86,6 @@ class SearchScreen(
PlaceListScreen( PlaceListScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer,
location,
it.id, it.id,
viewModel viewModel
) )
@@ -119,7 +115,7 @@ class SearchScreen(
object : SearchCallback { object : SearchCallback {
override fun onSearchSubmitted(searchTerm: String) { override fun onSearchSubmitted(searchTerm: String) {
isSearchComplete = true isSearchComplete = true
viewModel.searchPlaces(searchTerm, location) viewModel.searchPlaces(searchTerm, surfaceRenderer.lastLocation)
} }
}) })
.setHeaderAction(Action.BACK) .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.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.model.ViewModel
/** A screen demonstrating selectable lists. */ /** A screen demonstrating selectable lists. */
class SettingsScreen( class SettingsScreen(
carContext: CarContext, carContext: CarContext,
private var viewModel: ViewModel,
) : Screen(carContext) { ) : Screen(carContext) {
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -40,7 +42,7 @@ class SettingsScreen(
) )
listBuilder.addItem( listBuilder.addItem(
buildRowForTemplate( buildRowForTemplate(
NavigationSettings(carContext), NavigationSettings(carContext, viewModel),
R.string.navigation_settings 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 package com.kouros.navigation.car
import android.location.Location import com.kouros.navigation.data.valhalla.ValhallaRepository
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.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import org.junit.Test import org.junit.Test
@@ -16,23 +11,23 @@ import org.junit.Test
*/ */
class ViewModelTest { class ViewModelTest {
val repo = NavigationRepository() val repo = ValhallaRepository()
val viewModel = ViewModel(repo) val viewModel = ViewModel(repo)
val model = RouteModel() val model = RouteModel()
@Test @Test
fun routeViewModelTest() { fun routeViewModelTest() {
val fromLocation = Location(LocationManager.GPS_PROVIDER) // val fromLocation = Location(LocationManager.GPS_PROVIDER)
fromLocation.isMock = true // fromLocation.isMock = true
fromLocation.latitude = homeLocation.latitude // fromLocation.latitude = homeLocation.latitude
fromLocation.longitude = homeLocation.longitude // fromLocation.longitude = homeLocation.longitude
val toLocation = Location(LocationManager.GPS_PROVIDER) // val toLocation = Location(LocationManager.GPS_PROVIDER)
toLocation.isMock = true // toLocation.isMock = true
toLocation.latitude = home2Location.latitude // toLocation.latitude = home2Location.latitude
toLocation.longitude = home2Location.longitude // toLocation.longitude = home2Location.longitude
//
val route = repo.getRoute(fromLocation, toLocation, SearchFilter()) // val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
model.startNavigation(route) //model.startNavigation(route)
} }
} }

View File

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

View File

@@ -19,6 +19,8 @@ package com.kouros.navigation.data
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.net.Uri 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.Entity
import io.objectbox.annotation.Id import io.objectbox.annotation.Id
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -57,13 +59,16 @@ data class StepData (
var leftStepDistance: Double, var leftStepDistance: Double,
var maneuverType: Int, var currentManeuverType: Int,
var icon: Int, var icon: Int,
var arrivalTime : Long, 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 lat : Double,
var lon : Double, var lon : Double,
var street : String = "", var street : String = "",
val search_filter: SearchFilter, val search_filter: String,
) )
@Serializable
data class SearchFilter( data class SearchFilter(
var max_road_class: String = "", var avoidMotorway: Boolean = false,
var exclude_toll : 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 @Serializable
data class ValhallaLocation ( data class ValhallaLocation (
@@ -115,13 +96,6 @@ data class ValhallaLocation (
var language: String var language: String
) )
data class BoundingBox (
var southernLat : Double,
var westernLon: Double,
var northerLat : Double,
var easternLon : Double
)
object Constants { object Constants {
//const val STYLE: String = "https://kouros-online.de/liberty.json" //const val STYLE: String = "https://kouros-online.de/liberty.json"
@@ -149,17 +123,8 @@ object Constants {
val categories = listOf("Tankstelle", "Apotheke", "Ladestationen") val categories = listOf("Tankstelle", "Apotheke", "Ladestationen")
/** The initial location to use as an anchor for searches. */ /** The initial location to use as an anchor for searches. */
val homeLocation: Location = Location(LocationManager.GPS_PROVIDER) val homeVogelhart = location(11.5793748, 48.185749)
val home2Location: Location = Location(LocationManager.GPS_PROVIDER) val homeHohenwaldeck = location( 11.594322, 48.1164817)
init {
// Vogelhartstr. 17
homeLocation.latitude = 48.185749
homeLocation.longitude = 11.5793748
// Hohenwaldeckstr. 27
home2Location.latitude = 48.1164817
home2Location.longitude = 11.594322
}
const val SHARED_PREF_KEY = "NavigationPrefs" const val SHARED_PREF_KEY = "NavigationPrefs"
@@ -171,18 +136,24 @@ object Constants {
const val AVOID_TOLLWAY = "AvoidTollway" 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_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 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 { 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.content.Context
import android.location.Location import android.location.Location
import com.kouros.navigation.data.overpass.Elements import com.kouros.data.R
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import org.json.JSONArray import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import java.net.Authenticator import java.net.Authenticator
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.PasswordAuthentication import java.net.PasswordAuthentication
import java.net.URL import java.net.URL
import kotlinx.serialization.json.Json
abstract class NavigationRepository { abstract class NavigationRepository {
private val placesUrl = "https://kouros-online.de/maps/placespwd";
private val nominatimUrl = "https://nominatim.openstreetmap.org/" 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 { abstract fun getRoute(
val route = getRoute(currentLocation, location, searchFilter) context: Context,
val routeModel = RouteModel() currentLocation: Location,
routeModel.startNavigation(route, context) location: Location,
return routeModel.route.summary!!.distance 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 { fun searchPlaces(search: String, location: Location): String {
// val bbox = getBoundingBox(location.longitude, location.latitude, 10.0) val box = calculateSquareRadius(location.latitude, location.longitude, 100.0)
// val neLon = bbox["ne"]?.get("lon") val viewbox = "&bounded=1&viewbox=${box}"
// val neLat = bbox["ne"]?.get("lat") return fetchUrl(
// val swLon = bbox["sw"]?.get("lon") "${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
// val swLat = bbox["sw"]?.get("lat") true
// val viewbox = "&viewbox=$swLon,$swLat,$neLon,$neLat" )
return fetchUrl("${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true,&countrycodes=de", false)
} }
fun reverseAddress(location: Location) : String { fun reverseAddress(location: Location): String {
return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true&countrycodes=de", false) 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 { try {
if (authenticator) { if (authenticator) {
Authenticator.setDefault(object : Authenticator() { Authenticator.setDefault(object : Authenticator() {
@@ -102,7 +97,7 @@ abstract class NavigationRepository {
val responseCode = httpURLConnection.responseCode val responseCode = httpURLConnection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader() val response = httpURLConnection.inputStream.bufferedReader()
.use { it.readText() } // defaults to UTF-8 .use { it.readText() }
return response return response
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -1,18 +1,16 @@
package com.kouros.navigation.data package com.kouros.navigation.data
import android.location.Location
import com.google.gson.GsonBuilder 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.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute import com.kouros.navigation.data.osrm.OsrmRoute
import com.kouros.navigation.data.route.Leg 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.Step
import com.kouros.navigation.data.route.Summary 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.ValhallaResponse
import com.kouros.navigation.data.valhalla.ValhallaRoute 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 com.kouros.navigation.utils.location
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
@@ -23,34 +21,23 @@ import org.maplibre.geojson.Point
data class Route( data class Route(
val routeEngine: Int, val routeEngine: Int,
val summary: Summary?, val routes: List<com.kouros.navigation.data.route.Routes>,
val legs: List<Leg>?, var currentStepIndex: Int = 0,
val routeGeoJson: String = "",
val centerLocation: Location = location(0.0, 0.0),
var currentStep: Int = 0,
val waypoints: List<List<Double>>?,
) { ) {
data class Builder( data class Builder(
var routeEngine: Int = 0, var routeEngine: Int = 0,
var summary: Summary? = null, var summary: Summary = Summary(),
var legs: List<Leg>? = null, var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(),
var routeGeoJson: String = "",
var centerLocation: Location = location(0.0, 0.0),
var waypoints: List<List<Double>>? = null,
) { ) {
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine } fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun summary(summary: Summary) = apply { this.summary = summary } fun routes(routes: List<com.kouros.navigation.data.route.Routes>) = apply {
fun legs(legs: List<Leg>) = apply { this.legs = legs } this.routes = routes
fun routeGeoJson(routeGeoJson: String) = apply {
this.routeGeoJson = routeGeoJson
centerLocation = createCenterLocation(routeGeoJson)
}
}
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine } fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun waypoints(waypoints: List<List<Double>>) = apply { this.waypoints = waypoints }
fun route(route: String) = apply { fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") { if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
@@ -63,12 +50,15 @@ data class Route(
jsonObject["trip"].toString(), jsonObject["trip"].toString(),
ValhallaResponse::class.java ValhallaResponse::class.java
) )
ValhallaRoute().mapJsonToValhalla(routeJson, this) ValhallaRoute().mapToRoute(routeJson, this)
} }
RouteEngine.OSRM.ordinal -> {
else -> {
val osrmJson = gson.fromJson(route, OsrmResponse::class.java) 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 { fun build(): Route {
return Route( return Route(
routeEngine = this.routeEngine, routeEngine = this.routeEngine,
summary = this.summary, routes = this.routes,
legs = this.legs, )
waypoints = this.waypoints, }
routeGeoJson = this.routeGeoJson,
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> { fun maneuverLocations(): List<Point> {
val step = currentStep() val waypoints = currentStep().maneuver.waypoints
val waypoints = step.maneuver.waypoints
val points = mutableListOf<Point>() val points = mutableListOf<Point>()
for (loc in waypoints) { for (loc in waypoints) {
val point = Point.fromLngLat(loc[0], loc[1]) val point = Point.fromLngLat(loc[0], loc[1])
@@ -95,21 +117,4 @@ data class Route(
} }
return points 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( 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("entry") var entry: ArrayList<Boolean> = arrayListOf(),
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(), @SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(), @SerializedName("location") var location: ArrayList<Double> = arrayListOf(),

View File

@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
data class Legs ( data class Legs (
@SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(), @SerializedName("steps" ) var steps : List<Steps> = listOf(),
@SerializedName("weight" ) var weight : Double? = null, @SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("summary" ) var summary : String? = null, @SerializedName("summary" ) var summary : String = "",
@SerializedName("duration" ) var duration : Double? = null, @SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double? = null @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 import com.google.gson.annotations.SerializedName
data class Maneuver ( data class Maneuver(
@SerializedName("bearing_after" ) var bearingAfter : Int? = null, @SerializedName("bearing_after") var bearingAfter: Int = 0,
@SerializedName("bearing_before" ) var bearingBefore : Int? = null, @SerializedName("bearing_before") var bearingBefore: Int = 0,
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(), @SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
@SerializedName("modifier" ) var modifier : String? = null, @SerializedName("modifier") var modifier: String = "",
@SerializedName("type" ) var type : String? = null @SerializedName("type") var type: String = "",
@SerializedName("exit") var exit: Int = 0,
) )

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data.osrm package com.kouros.navigation.data.osrm
import android.content.Context
import android.location.Location import android.location.Location
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter 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() { class OsrmRepository : NavigationRepository() {
override fun getRoute( override fun getRoute(
context: Context,
currentLocation: Location, currentLocation: Location,
location: Location, location: Location,
carOrientation: Float,
searchFilter: SearchFilter searchFilter: SearchFilter
): String { ): 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 ( data class OsrmResponse (
@SerializedName("code" ) var code : String? = null, @SerializedName("code" ) var code : String = "",
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(), @SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf() @SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()

View File

@@ -1,44 +1,88 @@
package com.kouros.navigation.data.osrm package com.kouros.navigation.data.osrm
import com.kouros.navigation.data.Route 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.Leg
import com.kouros.navigation.data.route.Maneuver as RouteManeuver import com.kouros.navigation.data.route.Maneuver as RouteManeuver
import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Step
import com.kouros.navigation.data.route.Summary 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.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location
class OsrmRoute { class OsrmRoute {
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) { fun mapToRoute(routeJson: OsrmResponse, builder: Route.Builder) {
val waypoints = mutableListOf<List<Double>>()
val summary = Summary() val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
summary.distance = routeJson.routes.first().distance!!
summary.duration = routeJson.routes.first().duration!!
val steps = mutableListOf<Step>()
var stepIndex = 0 var stepIndex = 0
routeJson.routes.first().legs.first().steps.forEach { routeJson.routes.forEach { route ->
if (it.maneuver != null) { val legs = mutableListOf<Leg>()
val points = decodePolyline(it.geometry!!, 5) val waypoints = mutableListOf<List<Double>>()
waypoints.addAll(points) val summary = Summary(route.duration, route.distance / 1000)
val maneuver = RouteManeuver( route.legs.forEach { leg ->
bearingBefore = it.maneuver!!.bearingBefore ?: 0, val steps = mutableListOf<Step>()
bearingAfter = it.maneuver!!.bearingAfter ?: 0, leg.steps.forEach { step ->
type = convertType(it.maneuver!!), val intersections = mutableListOf<Intersection>()
waypoints = points val points = decodePolyline(step.geometry, 5)
) waypoints.addAll(points)
val step = Step( index = stepIndex, name = it.name!!, distance = it.distance!!, duration = it.duration!!, maneuver = maneuver) val maneuver = RouteManeuver(
steps.add(step) bearingBefore = step.maneuver.bearingBefore,
stepIndex += 1 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 builder
.routeType(1) .routeType(RouteEngine.OSRM.ordinal)
.summary(summary) .routes(routes)
.routeGeoJson(createLineStringCollection(waypoints))
.legs(listOf(leg))
.waypoints(waypoints.toList())
} }
fun convertType(maneuver: Maneuver): Int { fun convertType(maneuver: Maneuver): Int {
@@ -47,38 +91,138 @@ class OsrmRoute {
ManeuverType.depart.value -> { ManeuverType.depart.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
} }
ManeuverType.arrive.value -> { ManeuverType.arrive.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION 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 -> { ManeuverType.continue_.value -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
}
ManeuverType.turn.value -> {
if (maneuver.modifier == "right") { if (maneuver.modifier == "right") {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_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 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("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
@SerializedName("weight_name" ) var weightName : String? = null, @SerializedName("weight_name" ) var weightName : String? = null,
@SerializedName("geometry" ) var geometry : String? = null, @SerializedName("geometry" ) var geometry : String? = null,
@SerializedName("weight" ) var weight : Double? = null, @SerializedName("weight" ) var weight : Double = 0.0,
@SerializedName("duration" ) var duration : Double? = null, @SerializedName("duration" ) var duration : Double = 0.0,
@SerializedName("distance" ) var distance : Double? = null @SerializedName("distance" ) var distance : Double = 0.0
) )

View File

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

View File

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

View File

@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
data class Elements ( data class Elements (
@SerializedName("type" ) var type : String? = null, @SerializedName("type" ) var type : String = "",
@SerializedName("id" ) var id : Long? = null, @SerializedName("id" ) var id : Long = 0,
@SerializedName("lat" ) var lat : Double? = null, @SerializedName("lat" ) var lat : Double = 0.0,
@SerializedName("lon" ) var lon : Double? = null, @SerializedName("lon" ) var lon : Double = 0.0,
@SerializedName("tags" ) var tags : Tags = Tags(), @SerializedName("tags" ) var tags : Tags = Tags(),
var distance : Double = 0.0 var distance : Double = 0.0

View File

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

View File

@@ -18,7 +18,7 @@ data class Tags(
@SerializedName("ref") var ref: String? = null, @SerializedName("ref") var ref: String? = null,
@SerializedName("socket:type2") var socketType2: String? = null, @SerializedName("socket:type2") var socketType2: String? = null,
@SerializedName("socket:type2:output") var socketType2Output: String? = null, @SerializedName("socket:type2:output") var socketType2Output: String? = null,
@SerializedName("maxspeed") var maxspeed: String? = null, @SerializedName("maxspeed") var maxspeed: String = "0",
@SerializedName("direction") var direction: String? = null, @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 package com.kouros.navigation.data.route
data class Leg( 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 package com.kouros.navigation.data.route
import android.location.Location
data class Maneuver( data class Maneuver(
val bearingBefore : Int = 0, val bearingBefore : Int = 0,
val bearingAfter : Int = 0, val bearingAfter : Int = 0,
val type: Int = 0, val type: Int = 0,
val waypoints: List<List<Double>>, 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 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 index : Int = 0,
var waypointIndex : Int = 0, var waypointIndex : Int = 0,
var wayPointLocation : Location = location(0.0,0.0),
val maneuver: Maneuver, val maneuver: Maneuver,
val duration: Double = 0.0, val duration: Double = 0.0,
val distance: 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 package com.kouros.navigation.data.route
data class Summary( data class Summary(
// sec
var duration : Double = 0.0, var duration : Double = 0.0,
// km
var distance : Double = 0.0, 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 package com.kouros.navigation.data.valhalla
import android.content.Context
import android.location.Location import android.location.Location
import com.kouros.navigation.data.Locations import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
@@ -12,11 +13,29 @@ private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
class ValhallaRepository : NavigationRepository() { class ValhallaRepository : NavigationRepository() {
override fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String { override fun getRoute(
SearchFilter 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( val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter), Locations(
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter) lat = currentLocation.latitude,
lon = currentLocation.longitude,
search_filter = exclude
),
Locations(lat = location.latitude, lon = location.longitude, search_filter = exclude)
) )
val valhallaLocation = ValhallaLocation( val valhallaLocation = ValhallaLocation(
locations = vLocation, locations = vLocation,
@@ -28,4 +47,12 @@ class ValhallaRepository : NavigationRepository() {
val routeLocation = Json.encodeToString(valhallaLocation) val routeLocation = Json.encodeToString(valhallaLocation)
return fetchUrl(routeUrl + routeLocation, true) 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 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.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.Leg 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.Step
import com.kouros.navigation.data.route.Summary import com.kouros.navigation.data.route.Summary
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.decodePolyline import com.kouros.navigation.utils.GeoUtils.decodePolyline
import com.kouros.navigation.utils.location
class ValhallaRoute { class ValhallaRoute {
fun mapJsonToValhalla(routeJson: ValhallaResponse, builder: Route.Builder) { fun mapToRoute(routeJson: ValhallaResponse, builder: Route.Builder) {
val waypoints = decodePolyline(routeJson.legs[0].shape) val waypoints = decodePolyline(routeJson.legs[0].shape)
val summary = Summary() val summary = Summary(routeJson.summaryValhalla.time, routeJson.summaryValhalla.length)
summary.distance = routeJson.summaryValhalla.length
summary.duration = routeJson.summaryValhalla.time
val steps = mutableListOf<Step>() val steps = mutableListOf<Step>()
var stepIndex = 0 var stepIndex = 0
routeJson.legs[0].maneuvers.forEach { routeJson.legs[0].maneuvers.forEach {
val maneuver = Maneuver( val maneuver = RouteManeuver(
bearingBefore = 0, bearingBefore = 0,
bearingAfter = it.bearingAfter, bearingAfter = it.bearingAfter,
type = it.type, //type = it.type,
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1) type = convertType(it),
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1),
// TODO: calculate from ShapeIndex !
location = location(0.0, 0.0)
) )
var name = "" var name = ""
if (it.streetNames != null && it.streetNames.isNotEmpty()) { if (it.streetNames != null && it.streetNames.isNotEmpty()) {
name = it.streetNames[0] 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) steps.add(step)
stepIndex += 1 stepIndex += 1
} }
val leg = Leg(steps)
builder builder
.routeType(1) .routeType(RouteEngine.VALHALLA.ordinal)
.summary(summary) // TODO
.routeGeoJson(createLineStringCollection(waypoints)) .routes(emptyList())
.legs(listOf(leg)) }
.waypoints(waypoints)
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.content.Context
import android.location.Location import android.location.Location
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.Step
import com.kouros.data.R
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Constants.ROUTE_ENGINE import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.valhalla.ManeuverType
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.StepData 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.Leg
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope import kotlin.math.absoluteValue
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
open class RouteModel() { open class RouteModel {
data class RouteState(
val route: Route? = null, // Immutable Data Class
val isNavigating: Boolean = false, data class NavigationState(
val destination: Place = Place(), val route: Route = Route.Builder().buildEmpty(),
val iconMapper : IconMapper = IconMapper(),
val navigating: Boolean = false,
val arrived: Boolean = false, val arrived: Boolean = false,
val maneuverType: Int = 0,
val travelMessage: String = "", val travelMessage: String = "",
val lastSpeedLocation: Location = location(0.0, 0.0), val maneuverType: Int = 0,
val lastSpeedIndex: Int = 0, val lastLocation: Location = location(0.0, 0.0),
val maxSpeed: Int = 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 val route: Route
get() = routeState.route!! get() = navState.route
set(value) {
routeState = routeState.copy(route = value)
}
val routeCalculator : RouteCalculator = RouteCalculator(this)
val legs: Leg val curRoute: Routes
get() = routeState.route!!.legs!!.first() get() = navState.route.routes[navState.currentRouteIndex]
val curLeg: Leg
get() = navState.route.routes[navState.currentRouteIndex].legs.first()
fun startNavigation(routeString: String, context: Context) { fun startNavigation(routeString: String, context: Context) {
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE) val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
val newRoute = Route.Builder() navState = navState.copy(
.routeEngine(routeEngine) route = Route.Builder()
.route(routeString) .routeEngine(routeEngine)
.build() .route(routeString)
this.routeState = routeState.copy( .build()
route = newRoute,
isNavigating = true
) )
if (hasLegs()) {
navState = navState.copy(navigating = true)
}
}
private fun hasLegs(): Boolean {
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty()
} }
fun stopNavigation() { fun stopNavigation() {
this.routeState = routeState.copy( navState = navState.copy(
route = null, route = Route.Builder().buildEmpty(),
isNavigating = false, navigating = false,
arrived = false, arrived = false,
maneuverType = 0, maneuverType = Maneuver.TYPE_UNKNOWN
) )
} }
@OptIn(DelicateCoroutinesApi::class) fun updateLocation(curLocation: Location, viewModel: ViewModel) {
fun updateLocation(location: Location, viewModel: ViewModel) { navState = navState.copy(currentLocation = curLocation)
findStep(location) routeCalculator.findStep(curLocation)
updateSpeedLimit(location, viewModel) routeCalculator.updateSpeedLimit(curLocation, viewModel)
navState = navState.copy(lastLocation = navState.currentLocation)
} }
private fun findStep(location: Location) { private fun currentLanes(): List<Lane> {
var nearestDistance = 100000.0f var lanes = emptyList<Lane>()
for ((index, step) in legs.steps.withIndex()) { if (navState.route.legs().isNotEmpty()) {
if (index >= route.currentStep) { navState.route.legs().first().intersection.forEach {
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) { if (it.lane.isNotEmpty()) {
if (wayIndex >= step.waypointIndex) { val distance =
val distance = location.distanceTo(location(waypoint[0], waypoint[1])) navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
if (distance < nearestDistance) { val sectionBearing =
nearestDistance = distance navState.lastLocation.bearingTo(location(it.location[0], it.location[1]))
route.currentStep = step.index if (distance < 500 && (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
step.waypointIndex = wayIndex lanes = it.lane
}
}
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)
} }
} }
} }
} }
return lanes
} }
fun currentStep(): StepData { fun currentStep(): StepData {
val currentStep = route.currentStep() val distanceToNextStep = routeCalculator.leftStepDistance()
// 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)
// Determine the maneuver type and corresponding icon // Determine the maneuver type and corresponding icon
var maneuverType = if (hasArrived(currentStep.maneuver.type)) { val currentStep = navState.route.nextStep(0)
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()
}
// Safely get the street name from the maneuver // Safely get the street name from the maneuver
val streetName = relevantManeuver.name val streetName = currentStep.maneuver.street
if (shouldAdvance) { val curManeuverType = currentStep.maneuver.type
maneuverType = relevantManeuver.maneuver.type val exitNumber = currentStep.maneuver.exit
} val maneuverIcon = navState.iconMapper.maneuverIcon(curManeuverType)
val maneuverIconPair = maneuverIcon(maneuverType) navState = navState.copy(maneuverType = curManeuverType)
routeState = routeState.copy(maneuverType = maneuverIconPair.first)
val lanes = currentLanes()
// Construct and return the final StepData object // Construct and return the final StepData object
return StepData( return StepData(
streetName, streetName,
distanceToNextStep, distanceToNextStep,
maneuverIconPair.first, navState.maneuverType,
maneuverIconPair.second, maneuverIcon,
arrivalTime(), routeCalculator.arrivalTime(),
travelLeftDistance() routeCalculator.travelLeftDistance(),
lanes,
exitNumber
) )
} }
fun nextStep(): StepData { fun nextStep(): StepData {
val step = route.nextStep() val step = navState.route.nextStep(1)
val maneuverType = step.maneuver.type val maneuverType = step.maneuver.type
val distanceLeft = leftStepDistance() val distanceLeft = routeCalculator.leftStepDistance()
var text = "" var text = ""
when (distanceLeft) { when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> { in 0.0..NEXT_STEP_THRESHOLD -> {
} }
else -> { else -> {
if (step.name.isNotEmpty()) { if (step.street.isNotEmpty()) {
text = step.name text = step.street
} }
} }
} }
val routing: (Pair<Int, Int>) = maneuverIcon(maneuverType) val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object // Construct and return the final StepData object
return StepData( return StepData(
text, text,
distanceLeft, distanceLeft,
routing.first, maneuverType,
routing.second, maneuverIcon,
arrivalTime(), routeCalculator.arrivalTime(),
travelLeftDistance() 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 { fun isNavigating(): Boolean {
return routeState.isNavigating return navState.navigating
}
fun hasArrived(type: Int): Boolean {
return type == ManeuverType.DestinationRight.value
|| type == ManeuverType.Destination.value
|| type == ManeuverType.DestinationLeft.value
} }
} }

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.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass import com.kouros.navigation.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.NavigationUtils
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import io.objectbox.kotlin.boxFor import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.maplibre.geojson.FeatureCollection
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -32,6 +38,11 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData() MutableLiveData()
} }
val traffic: MutableLiveData<Map<String, String>> by lazy {
MutableLiveData()
}
val previewRoute: MutableLiveData<String> by lazy { val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData() MutableLiveData()
} }
@@ -54,7 +65,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val placeLocation: MutableLiveData<SearchResult> by lazy { val placeLocation: MutableLiveData<SearchResult> by lazy {
MutableLiveData() MutableLiveData()
} }
val contactAddress: MutableLiveData<List<Place>> by lazy { val contactAddress: MutableLiveData<List<Place>> by lazy {
MutableLiveData() MutableLiveData()
} }
@@ -67,7 +78,14 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
MutableLiveData() 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) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
@@ -79,12 +97,18 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
query.close() query.close()
for (place in results) { for (place in results) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
//val distance = repository.getRouteDistance(location, plLocation, SearchFilter()) val distance = repository.getRouteDistance(
//place.distance = distance.toFloat() location,
//if (place.distance == 0F) { plLocation,
recentPlace.postValue(place) carOrientation,
return@launch SearchFilter(),
//} context
)
place.distance = distance.toFloat()
if (place.distance > 1F) {
recentPlace.postValue(place)
return@launch
}
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() 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) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
@@ -104,15 +128,16 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
query.close() query.close()
for (place in results) { for (place in results) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) { if (place.latitude != 0.0) {
val distance = val distance =
repository.getRouteDistance( repository.getRouteDistance(
location, location,
plLocation, plLocation,
getSearchFilter(context), context carOrientation,
) getSearchFilter(context), context
place.distance = distance.toFloat() )
} place.distance = distance.toFloat()
}
} }
places.postValue(results) places.postValue(results)
} catch (e: Exception) { } 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) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
@@ -134,7 +159,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
for (place in results) { for (place in results) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
val distance = val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context) repository.getRouteDistance(
location,
plLocation,
carOrientation,
getSearchFilter(context),
context
)
place.distance = distance.toFloat() place.distance = distance.toFloat()
} }
favorites.postValue(results) 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) { viewModelScope.launch(Dispatchers.IO) {
try { try {
route.postValue( route.postValue(
repository.getRoute( repository.getRoute(
context,
currentLocation, currentLocation,
location, location,
carOrientation,
getSearchFilter(context) 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) { viewModelScope.launch(Dispatchers.IO) {
try { try {
previewRoute.postValue( previewRoute.postValue(
repository.getRoute( repository.getRoute(
context,
currentLocation, currentLocation,
location, location,
carOrientation,
getSearchFilter(context) getSearchFilter(context)
) )
) )
@@ -224,21 +309,24 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
} }
} }
} }
fun searchPlaces(search: String, location: Location) { fun searchPlaces(search: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location) val placesJson = repository.searchPlaces(search, location)
val gson = GsonBuilder().serializeNulls().create() if (placesJson.isNotEmpty()) {
val places = gson.fromJson(placesJson, Search::class.java) val gson = GsonBuilder().serializeNulls().create()
val distPlaces = mutableListOf<SearchResult>() val places = gson.fromJson(placesJson, Search::class.java)
places.forEach { val distPlaces = mutableListOf<SearchResult>()
val plLocation = places.forEach {
location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble()) val plLocation =
val distance = plLocation.distanceTo(location) location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble())
it.distance = distance val distance = plLocation.distanceTo(location)
distPlaces.add(it) 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) { viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius) val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
val distAmenities = mutableListOf<Elements>() val distAmenities = mutableListOf<Elements>()
amenities.forEach { amenities.forEach {
val plLocation = val plLocation =
location(longitude = it.lon!!, latitude = it.lat!!) location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location) val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble() it.distance = distance.toDouble()
distAmenities.add(it) 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 lineString = "${location.latitude},${location.longitude}"
val amenities = Overpass().getAround(10, lineString) 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) { fun saveFavorite(place: Place) {
@@ -293,6 +393,12 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
} }
fun saveRecent(place: Place) { fun saveRecent(place: Place) {
if (place.category == Constants.FUEL_STATION
|| place.category == Constants.CHARGING_STATION
|| place.category == Constants.PHARMACY
) {
return
}
place.category = Constants.RECENT place.category = Constants.RECENT
savePlace(place) savePlace(place)
} }
@@ -351,7 +457,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
} }
fun getSearchFilter(context: Context): SearchFilter { fun getSearchFilter(context: Context): SearchFilter {
val avoidMotorway = NavigationUtils.getBooleanKeyValue( val avoidMotorway = NavigationUtils.getBooleanKeyValue(
context = context, context = context,
Constants.AVOID_MOTORWAY Constants.AVOID_MOTORWAY
@@ -360,14 +465,15 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
context = context, context = context,
Constants.AVOID_TOLLWAY Constants.AVOID_TOLLWAY
) )
return SearchFilter.Builder() return SearchFilter(avoidMotorway, avoidTollway)
.avoidMotorway(avoidMotorway)
.avoidTollway(avoidTollway)
.build()
} }
fun loadPlaces2(context: Context, location: Location): SnapshotStateList<Place?> { fun loadPlaces2(
context: Context,
location: Location,
carOrientation: Float
): SnapshotStateList<Place?> {
val results = listOf<Place>() val results = listOf<Place>()
try { try {
val placeBox = boxStore.boxFor(Place::class) val placeBox = boxStore.boxFor(Place::class)
@@ -380,7 +486,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
for (place in results) { for (place in results) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
val distance = val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context) repository.getRouteDistance(
location,
plLocation,
carOrientation,
getSearchFilter(context),
context
)
place.distance = distance.toFloat() place.distance = distance.toFloat()
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

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

View File

@@ -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.Location
import android.location.LocationManager import android.location.LocationManager
import androidx.core.content.edit 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.Constants.SHARED_PREF_KEY
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository 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.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.ViewModel import com.kouros.navigation.model.ViewModel
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -25,11 +26,12 @@ import kotlin.time.Duration.Companion.seconds
object NavigationUtils { object NavigationUtils {
fun getRouteEngine(context: Context): ViewModel { fun getViewModel(context: Context): ViewModel {
val routeEngine = getIntKeyValue(context = context, ROUTE_ENGINE) val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
return when (routeEngine) { return when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository()) 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 in 61..70 -> 16.5
else -> 16.0 else -> 16.0
} }
return zoom.toDouble() return zoom
} }
fun previewZoom(previewDistance: Double): Double { fun previewZoom(previewDistance: Double): Double {
when (previewDistance) { when (previewDistance) {
in 0.0..10.0 -> return 13.0 in 0.0..10.0 -> return 13.5
in 10.0..20.0 -> return 11.0 in 10.0..20.0 -> return 11.5
in 20.0..30.0 -> return 10.0 in 20.0..30.0 -> return 10.5
} }
return 9.0 return 9.5
} }
fun calculateTilt(newZoom: Double, tilt: Double): Double = 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"> android:tint="?attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" 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> </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