Compare commits
9 Commits
fdf2ee9f48
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65ff41995d | ||
|
|
5141041b5c | ||
|
|
e9474695bf | ||
|
|
0d51c6121d | ||
|
|
eac5b56bcb | ||
|
|
7db7cba4fb | ||
|
|
e22865bd73 | ||
|
|
e274011080 | ||
|
|
7efa2685be |
200
CLAUDE.md
Normal 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
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.kouros.navigation"
|
||||
minSdk = 33
|
||||
targetSdk = 36
|
||||
versionCode = 18
|
||||
versionName = "0.1.3.18"
|
||||
versionCode = 38
|
||||
versionName = "0.2.0.38"
|
||||
base.archivesName = "navi-$versionName"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -94,7 +94,13 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.window)
|
||||
implementation(libs.androidx.compose.foundation.layout)
|
||||
|
||||
implementation("com.github.ticofab:android-gpx-parser:2.3.1")
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation("com.github.alorma.compose-settings:ui-tiles:2.25.0")
|
||||
implementation("com.github.alorma.compose-settings:ui-tiles-extended:2.25.0")
|
||||
implementation("com.github.alorma.compose-settings:ui-tiles-expressive:2.25.0")
|
||||
implementation(libs.androidx.foundation.layout)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context
|
||||
import com.kouros.navigation.data.ObjectBox
|
||||
import com.kouros.navigation.di.appModule
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
|
||||
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
@@ -17,7 +17,7 @@ class MainApplication : Application() {
|
||||
super.onCreate()
|
||||
ObjectBox.init(this);
|
||||
appContext = applicationContext
|
||||
navigationViewModel = getRouteEngine(appContext!!)
|
||||
navigationViewModel = getViewModel(appContext!!)
|
||||
startKoin {
|
||||
androidLogger(Level.DEBUG)
|
||||
androidContext(this@MainApplication)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.kouros.navigation.di
|
||||
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
import com.kouros.navigation.data.osrm.OsrmRepository
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
@@ -14,4 +15,5 @@ val appModule = module {
|
||||
viewModelOf(::ViewModel)
|
||||
singleOf(::ValhallaRepository)
|
||||
singleOf(::OsrmRepository)
|
||||
singleOf(::TomTomRepository)
|
||||
}
|
||||
@@ -48,8 +48,8 @@ class MockLocation (private var locationManager: LocationManager) {
|
||||
this.latitude = latitude
|
||||
this.longitude = longitude
|
||||
this.altitude = 0.0
|
||||
this.accuracy = 1.0f
|
||||
this.speed = 10f
|
||||
this.accuracy = 0f
|
||||
this.speed = 0f
|
||||
this.time = System.currentTimeMillis()
|
||||
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||
|
||||
@@ -71,8 +71,8 @@ class MockLocation (private var locationManager: LocationManager) {
|
||||
this.latitude = latitude
|
||||
this.longitude = longitude
|
||||
this.altitude = 0.0
|
||||
this.accuracy = 1.0f
|
||||
this.speed = 10f
|
||||
this.accuracy = 0f
|
||||
this.speed = 0f
|
||||
this.time = System.currentTimeMillis()
|
||||
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import NavigationSheet
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AppOpsManager
|
||||
import android.content.Context
|
||||
import android.location.LocationManager
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
@@ -14,9 +15,13 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.BottomSheetScaffold
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
@@ -32,37 +37,47 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||
import com.kouros.navigation.data.Constants.homeLocation
|
||||
import com.kouros.navigation.data.Constants.homeVogelhart
|
||||
import com.kouros.navigation.data.StepData
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
import com.kouros.navigation.model.MockLocation
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.ui.theme.NavigationTheme
|
||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||
import com.kouros.navigation.utils.bearing
|
||||
import com.kouros.navigation.utils.calculateZoom
|
||||
import com.kouros.navigation.utils.location
|
||||
import io.ticofab.androidgpxparser.parser.GPXParser
|
||||
import io.ticofab.androidgpxparser.parser.domain.Gpx
|
||||
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.Location
|
||||
import org.maplibre.compose.location.rememberDefaultLocationProvider
|
||||
import org.maplibre.compose.location.rememberUserLocationState
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
import org.maplibre.geojson.Point
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -72,7 +87,10 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
val routeModel = RouteModel()
|
||||
var tilt = 50.0
|
||||
val useMock = true
|
||||
val useMock = false
|
||||
val type = 3 // simulate 2 test 3 gpx 4 testSingle
|
||||
|
||||
var currentIndex = 0
|
||||
val stepData: MutableLiveData<StepData> by lazy {
|
||||
MutableLiveData<StepData>()
|
||||
}
|
||||
@@ -83,15 +101,23 @@ class MainActivity : ComponentActivity() {
|
||||
val observer = Observer<String> { newRoute ->
|
||||
if (newRoute.isNotEmpty()) {
|
||||
routeModel.startNavigation(newRoute, applicationContext)
|
||||
routeData.value = routeModel.route.routeGeoJson
|
||||
simulate()
|
||||
//test()
|
||||
routeData.value = routeModel.curRoute.routeGeoJson
|
||||
if (useMock) {
|
||||
when (type) {
|
||||
1 -> simulate()
|
||||
2 -> test()
|
||||
3 -> gpx(
|
||||
context = applicationContext
|
||||
)
|
||||
|
||||
4 -> testSingle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val cameraPosition = MutableLiveData(
|
||||
CameraPosition(
|
||||
zoom = 15.0,
|
||||
target = Position(latitude = 48.1857475, longitude = 11.5793627)
|
||||
zoom = 15.0, target = Position(latitude = 48.1857475, longitude = 11.5793627)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -102,13 +128,8 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private var loadRecentPlaces = false
|
||||
|
||||
private var overpass = false
|
||||
|
||||
lateinit var baseStyle: BaseStyle.Json
|
||||
|
||||
init {
|
||||
navigationViewModel.route.observe(this, observer)
|
||||
}
|
||||
|
||||
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -120,16 +141,15 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
|
||||
fusedLocationClient.lastLocation
|
||||
.addOnSuccessListener { location: android.location.Location? ->
|
||||
if (useMock) {
|
||||
mock = MockLocation(locationManager)
|
||||
mock.setMockLocation(
|
||||
location?.latitude ?: homeLocation.latitude,
|
||||
location?.longitude ?: homeLocation.longitude
|
||||
)
|
||||
}
|
||||
fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? ->
|
||||
if (useMock) {
|
||||
mock = MockLocation(locationManager)
|
||||
mock.setMockLocation(
|
||||
homeVogelhart.latitude, homeVogelhart.longitude
|
||||
)
|
||||
navigationViewModel.route.observe(this, observer)
|
||||
}
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
CheckPermissionScreen()
|
||||
@@ -147,23 +167,35 @@ class MainActivity : ComponentActivity() {
|
||||
permissions = permissions,
|
||||
requiredPermissions = listOf(permissions.first()),
|
||||
onGranted = {
|
||||
Content()
|
||||
App()
|
||||
// auto navigate
|
||||
if (useMock) {
|
||||
// navigationViewModel.loadRoute(
|
||||
// applicationContext,
|
||||
// homeVogelhart,
|
||||
// homeHohenwaldeck,
|
||||
// 0F
|
||||
// )
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("AutoboxingStateCreation")
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Content() {
|
||||
fun StartScreen(
|
||||
navController: NavHostController
|
||||
|
||||
) {
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val sheetPeekHeightState = remember { mutableStateOf(256.dp) }
|
||||
|
||||
val locationProvider = rememberDefaultLocationProvider(
|
||||
updateInterval = 0.5.seconds,
|
||||
desiredAccuracy = DesiredAccuracy.Highest
|
||||
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
|
||||
)
|
||||
val userLocationState = rememberUserLocationState(locationProvider)
|
||||
val locationState = locationProvider.location.collectAsState()
|
||||
@@ -201,55 +233,82 @@ class MainActivity : ComponentActivity() {
|
||||
applicationContext,
|
||||
userLocationState,
|
||||
step,
|
||||
nextStep,
|
||||
cameraPosition,
|
||||
routeData,
|
||||
tilt,
|
||||
baseStyle
|
||||
)
|
||||
}
|
||||
if (!routeModel.isNavigating()) {
|
||||
Settings(navController, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun App() {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController = navController, startDestination = "startScreen") {
|
||||
composable("startScreen") { StartScreen(navController) }
|
||||
composable("settings") { SettingsScreen(navController) { navController.popBackStack() } }
|
||||
composable("display_settings") { DisplayScreenSettings(applicationContext) { navController.popBackStack() } }
|
||||
composable("nav_settings") { NavigationScreenSettings(applicationContext) { navController.popBackStack() } }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Settings(navController: NavController, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.padding(start = 10.dp, top = 40.dp),
|
||||
onClick = {
|
||||
navController.navigate("settings")
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.menu_24px),
|
||||
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SheetContent(
|
||||
locationState: Double,
|
||||
step: StepData?,
|
||||
nextStep: StepData?,
|
||||
closeSheet: () -> Unit
|
||||
locationState: Double, step: StepData?, nextStep: StepData?, closeSheet: () -> Unit
|
||||
) {
|
||||
if (!routeModel.isNavigating()) {
|
||||
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() }
|
||||
} else {
|
||||
NavigationSheet(
|
||||
routeModel, step!!, nextStep!!,
|
||||
applicationContext,
|
||||
routeModel,
|
||||
step!!,
|
||||
nextStep!!,
|
||||
{ stopNavigation { closeSheet() } },
|
||||
{ simulateNavigation() }
|
||||
)
|
||||
{ simulateNavigation() })
|
||||
}
|
||||
// For recomposition!
|
||||
Text("$locationState", fontSize = 12.sp)
|
||||
}
|
||||
|
||||
fun updateLocation(location: Location?) {
|
||||
if (location != null
|
||||
&& lastLocation.latitude != location.position.latitude
|
||||
&& lastLocation.longitude != location.position.longitude
|
||||
) {
|
||||
if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) {
|
||||
val currentLocation = location(location.position.longitude, location.position.latitude)
|
||||
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
|
||||
with(routeModel) {
|
||||
if (isNavigating()) {
|
||||
updateLocation(currentLocation, navigationViewModel)
|
||||
stepData.value = currentStep()
|
||||
if (route.currentStep + 1 <= legs.steps.size) {
|
||||
nextStepData.value = nextStep()
|
||||
}
|
||||
if (routeState.maneuverType == 39
|
||||
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||
) {
|
||||
nextStepData.value = nextStep()
|
||||
if (navState.maneuverType in 39..42 && routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) {
|
||||
// stopNavigation()
|
||||
routeState = routeState.copy(arrived = true)
|
||||
navState.copy(arrived = true)
|
||||
routeData.value = ""
|
||||
}
|
||||
}
|
||||
@@ -257,22 +316,25 @@ class MainActivity : ComponentActivity() {
|
||||
val zoom = calculateZoom(location.speed)
|
||||
cameraPosition.postValue(
|
||||
cameraPosition.value!!.copy(
|
||||
zoom = zoom,
|
||||
target = location.position,
|
||||
bearing = bearing
|
||||
zoom = zoom, target = location.position, bearing = bearing
|
||||
),
|
||||
)
|
||||
lastLocation = currentLocation
|
||||
if (!loadRecentPlaces) {
|
||||
navigationViewModel.loadRecentPlaces(applicationContext, lastLocation)
|
||||
navigationViewModel.loadRecentPlaces(applicationContext, lastLocation, 0F)
|
||||
loadRecentPlaces = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopNavigation(closeSheet: () -> Unit) {
|
||||
val latitude = routeModel.curRoute.waypoints[0][1]
|
||||
val longitude = routeModel.curRoute.waypoints[0][0]
|
||||
closeSheet()
|
||||
routeModel.stopNavigation()
|
||||
if (useMock) {
|
||||
mock.setMockLocation(latitude, longitude)
|
||||
}
|
||||
routeData.value = ""
|
||||
stepData.value = StepData("", 0.0, 0, 0, 0, 0.0)
|
||||
}
|
||||
@@ -284,14 +346,10 @@ class MainActivity : ComponentActivity() {
|
||||
private fun checkMockLocationEnabled() {
|
||||
try {
|
||||
// Check if mock location is enabled for this app
|
||||
val appOpsManager =
|
||||
getSystemService(APP_OPS_SERVICE) as AppOpsManager
|
||||
val mode =
|
||||
appOpsManager.checkOp(
|
||||
AppOpsManager.OPSTR_MOCK_LOCATION,
|
||||
Process.myUid(),
|
||||
packageName
|
||||
)
|
||||
val appOpsManager = getSystemService(APP_OPS_SERVICE) as AppOpsManager
|
||||
val mode = appOpsManager.checkOp(
|
||||
AppOpsManager.OPSTR_MOCK_LOCATION, Process.myUid(), packageName
|
||||
)
|
||||
|
||||
if (mode != AppOpsManager.MODE_ALLOWED) {
|
||||
Toast.makeText(
|
||||
@@ -307,26 +365,80 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
fun simulate() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (routeModel.isNavigating()) {
|
||||
for ((index, waypoint) in routeModel.route.waypoints!!.withIndex()) {
|
||||
var deviation = 0.0
|
||||
mock.setMockLocation(waypoint[1] + deviation, waypoint[0])
|
||||
delay(500L) //
|
||||
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
|
||||
if (routeModel.isNavigating()) {
|
||||
val deviation = 0.0
|
||||
if (index in 0..routeModel.curRoute.waypoints.size) {
|
||||
mock.setMockLocation(waypoint[1], waypoint[0])
|
||||
Thread.sleep(500)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun test() {
|
||||
for ((index, step) in routeModel.legs.steps.withIndex()) {
|
||||
println("${step.maneuver.waypoints.size}")
|
||||
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
|
||||
//if (index in 3..3) {
|
||||
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||
routeModel.updateLocation(location(waypoint[0], waypoint[1]), navigationViewModel)
|
||||
routeModel.currentStep()
|
||||
if (index + 1 <= routeModel.legs.steps.size) {
|
||||
nextStepData.value = routeModel.nextStep()
|
||||
routeModel.updateLocation(
|
||||
location(waypoint[0], waypoint[1]), navigationViewModel
|
||||
)
|
||||
val step = routeModel.currentStep()
|
||||
val nextStep = routeModel.nextStep()
|
||||
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
fun testSingle() {
|
||||
testSingleUpdate(48.185976, 11.578463) // Silcherstr. 23-13
|
||||
testSingleUpdate(48.186712, 11.578574) // Silcherstr. 27-33
|
||||
testSingleUpdate(48.186899, 11.580480) // Schmalkadenerstr. 24-28
|
||||
}
|
||||
|
||||
fun testSingleUpdate(latitude: Double, longitude: Double) {
|
||||
if (1 == 1) {
|
||||
mock.setMockLocation(latitude, longitude)
|
||||
} else {
|
||||
routeModel.updateLocation(
|
||||
location(longitude, latitude), navigationViewModel
|
||||
)
|
||||
}
|
||||
val step = routeModel.currentStep()
|
||||
val nextStep = routeModel.nextStep()
|
||||
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
|
||||
Thread.sleep(1_000)
|
||||
}
|
||||
|
||||
fun gpx(context: Context) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val parser = GPXParser()
|
||||
val input = context.resources.openRawResource(R.raw.vh)
|
||||
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
|
||||
parsedGpx?.let {
|
||||
val tracks = parsedGpx.tracks
|
||||
tracks.forEach { tr ->
|
||||
val segments: MutableList<TrackSegment?>? = tr.trackSegments
|
||||
segments!!.forEach { seg ->
|
||||
var lastTime = DateTime.now()
|
||||
seg!!.trackPoints.forEach { p ->
|
||||
val ext = p.extensions
|
||||
val speed: Double?
|
||||
if (ext != null) {
|
||||
speed = ext.speed
|
||||
mock.curSpeed = speed.toFloat()
|
||||
}
|
||||
val duration = p.time.millis - lastTime.millis
|
||||
mock.setMockLocation(p.latitude, p.longitude)
|
||||
if (duration > 0) {
|
||||
delay(duration / 5)
|
||||
}
|
||||
lastTime = p.time
|
||||
}
|
||||
}
|
||||
}
|
||||
println(routeModel.routeState.maneuverType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.kouros.navigation.car.map.MapLibre
|
||||
import com.kouros.navigation.car.map.NavigationImage
|
||||
import com.kouros.navigation.car.map.rememberBaseStyle
|
||||
import com.kouros.navigation.data.StepData
|
||||
import com.kouros.navigation.data.tomtom.TrafficData
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
import org.maplibre.compose.location.LocationTrackingEffect
|
||||
@@ -29,6 +30,7 @@ fun MapView(
|
||||
applicationContext: Context,
|
||||
userLocationState: UserLocationState,
|
||||
step: StepData?,
|
||||
nextStep: StepData?,
|
||||
cameraPosition: MutableLiveData<CameraPosition>,
|
||||
routeData: MutableLiveData<String>,
|
||||
tilt: Double,
|
||||
@@ -57,13 +59,14 @@ fun MapView(
|
||||
|
||||
val rememberBaseStyle = rememberBaseStyle( baseStyle)
|
||||
Column {
|
||||
NavigationInfo(step)
|
||||
NavigationInfo(step, nextStep)
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
MapLibre(
|
||||
applicationContext,
|
||||
cameraState,
|
||||
rememberBaseStyle,
|
||||
route,
|
||||
emptyMap(),
|
||||
ViewStyle.VIEW
|
||||
)
|
||||
LocationTrackingEffect(
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.kouros.navigation.data.StepData
|
||||
import com.kouros.navigation.utils.round
|
||||
|
||||
@Composable
|
||||
fun NavigationInfo(step: StepData?) {
|
||||
fun NavigationInfo(step: StepData?, nextStep: StepData?) {
|
||||
if (step != null && step.instruction.isNotEmpty()) {
|
||||
Card(modifier = Modifier.padding(top = 60.dp)) {
|
||||
Column() {
|
||||
@@ -28,6 +28,10 @@ fun NavigationInfo(step: StepData?) {
|
||||
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||
modifier = Modifier.size(48.dp, 48.dp),
|
||||
)
|
||||
if (step.currentManeuverType == 46
|
||||
|| step.currentManeuverType == 45) {
|
||||
Text(text ="Exit ${step.exitNumber}", fontSize = 20.sp)
|
||||
}
|
||||
Column {
|
||||
if (step.leftStepDistance < 1000) {
|
||||
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp)
|
||||
@@ -39,11 +43,13 @@ fun NavigationInfo(step: StepData?) {
|
||||
}
|
||||
Text(text = step.instruction, fontSize = 20.sp)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(step.icon),
|
||||
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||
modifier = Modifier.size(48.dp, 48.dp),
|
||||
)
|
||||
if (nextStep != null && step.icon != nextStep.icon) {
|
||||
Icon(
|
||||
painter = painterResource(nextStep.icon),
|
||||
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||
modifier = Modifier.size(48.dp, 48.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
||||
import com.kouros.navigation.data.StepData
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.formatDateTime
|
||||
@@ -23,19 +24,26 @@ import com.kouros.navigation.utils.round
|
||||
|
||||
@Composable
|
||||
fun NavigationSheet(
|
||||
applicationContext: Context,
|
||||
routeModel: RouteModel,
|
||||
step: StepData,
|
||||
nextStep: StepData,
|
||||
stopNavigation: () -> Unit,
|
||||
simulateNavigation: () -> Unit,
|
||||
) {
|
||||
val distance = step.leftDistance.round(1)
|
||||
val distance = (step.leftDistance / 1000).round(1)
|
||||
|
||||
if (step.lane.isNotEmpty()) {
|
||||
routeModel.navState.iconMapper.addLanes( step)
|
||||
}
|
||||
|
||||
Column {
|
||||
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Text(formatDateTime(step.arrivalTime), fontSize = 22.sp)
|
||||
Spacer(Modifier.size(30.dp))
|
||||
Text("$distance km", fontSize = 22.sp)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
if (routeModel.isNavigating()) {
|
||||
@@ -48,6 +56,15 @@ fun NavigationSheet(
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
)
|
||||
}
|
||||
Button(onClick = {
|
||||
simulateNavigation()
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_zoom_in_24),
|
||||
"Stop",
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.size(30.dp))
|
||||
if (!routeModel.isNavigating()) {
|
||||
|
||||
@@ -57,38 +57,31 @@ fun SearchSheet(
|
||||
if (search.value != null) {
|
||||
searchResults.addAll(search.value!!)
|
||||
}
|
||||
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
||||
if (searchResults.isNotEmpty()) {
|
||||
val textFieldState = rememberTextFieldState()
|
||||
val items = listOf(searchResults)
|
||||
if (items.isNotEmpty()) {
|
||||
SearchBar(
|
||||
textFieldState = textFieldState,
|
||||
searchPlaces = recentPlaces.value!!,
|
||||
searchResults = searchResults,
|
||||
viewModel = viewModel,
|
||||
context = applicationContext,
|
||||
location = location,
|
||||
closeSheet = { closeSheet() }
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
||||
if (recentPlaces.value != null) {
|
||||
val textFieldState = rememberTextFieldState()
|
||||
val items = listOf(recentPlaces)
|
||||
if (items.isNotEmpty()) {
|
||||
SearchBar(
|
||||
textFieldState = textFieldState,
|
||||
searchPlaces = recentPlaces.value!!,
|
||||
searchResults = searchResults,
|
||||
viewModel = viewModel,
|
||||
context = applicationContext,
|
||||
location = location,
|
||||
closeSheet = { closeSheet() }
|
||||
)
|
||||
RecentPlaces(recentPlaces.value!!, viewModel, applicationContext, location, closeSheet)
|
||||
}
|
||||
}
|
||||
// if (searchResults.isNotEmpty()) {
|
||||
val textFieldState = rememberTextFieldState()
|
||||
val items = listOf(searchResults)
|
||||
// if (items.isNotEmpty()) {
|
||||
SearchBar(
|
||||
textFieldState = textFieldState,
|
||||
searchPlaces = emptyList<Place>(),
|
||||
searchResults = searchResults,
|
||||
viewModel = viewModel,
|
||||
context = applicationContext,
|
||||
location = location,
|
||||
closeSheet = { closeSheet() }
|
||||
|
||||
)
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -103,7 +96,7 @@ fun Home(
|
||||
Button(onClick = {
|
||||
val places = viewModel.loadRecentPlace()
|
||||
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
||||
viewModel.loadRoute(applicationContext, location, toLocation)
|
||||
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
|
||||
closeSheet()
|
||||
}) {
|
||||
Icon(
|
||||
@@ -138,15 +131,7 @@ fun SearchBar(
|
||||
closeSheet: () -> Unit
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.semantics { isTraversalGroup = true }
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.semantics { traversalIndex = 0f },
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
leadingIcon = {
|
||||
@@ -179,7 +164,6 @@ fun SearchBar(
|
||||
SearchPlaces(searchResults, viewModel, context, location, closeSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
|
||||
@@ -223,7 +207,7 @@ private fun SearchPlaces(
|
||||
viewModel.saveRecent(pl)
|
||||
val toLocation =
|
||||
location(place.lon.toDouble(), place.lat.toDouble())
|
||||
viewModel.loadRoute(context, location, toLocation)
|
||||
viewModel.loadRoute(context, location, toLocation, 0F)
|
||||
closeSheet()
|
||||
}
|
||||
.fillMaxWidth()
|
||||
@@ -261,7 +245,7 @@ private fun RecentPlaces(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
val toLocation = location(place.longitude, place.latitude)
|
||||
viewModel.loadRoute(context, location, toLocation)
|
||||
viewModel.loadRoute(context, location, toLocation, 0F)
|
||||
closeSheet()
|
||||
}
|
||||
.fillMaxWidth()
|
||||
|
||||
86
app/src/main/java/com/kouros/navigation/ui/SettingsScreen.kt
Normal 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),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,75 @@ package com.kouros.navigation.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
//val Purple80 = Color(0xFFD0BCFF)
|
||||
//val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
//val Pink80 = Color(0xFFEFB8C8)
|
||||
//
|
||||
//val Purple40 = Color(0xFF6650a4)
|
||||
//val PurpleGrey40 = Color(0xFF625b71)
|
||||
//val Pink40 = Color(0xFF7D5260)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
val md_theme_light_primary = Color(0xFF825500)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
|
||||
val md_theme_light_secondary = Color(0xFF6F5B40)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
|
||||
val md_theme_light_tertiary = Color(0xFF51643F)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
|
||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
val md_theme_light_onBackground = Color(0xFF1F1B16)
|
||||
val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
val md_theme_light_onSurface = Color(0xFF1F1B16)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
|
||||
val md_theme_light_outline = Color(0xFF817567)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
|
||||
val md_theme_light_inverseSurface = Color(0xFF34302A)
|
||||
val md_theme_light_inversePrimary = Color(0xFFFFB951)
|
||||
val md_theme_light_shadow = Color(0xFF000000)
|
||||
val md_theme_light_surfaceTint = Color(0xFF825500)
|
||||
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
|
||||
val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
val md_theme_dark_primary = Color(0xFFFFB951)
|
||||
val md_theme_dark_onPrimary = Color(0xFF452B00)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF633F00)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
|
||||
val md_theme_dark_secondary = Color(0xFFDDC2A1)
|
||||
val md_theme_dark_onSecondary = Color(0xFF3E2D16)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF56442A)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
|
||||
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
|
||||
val md_theme_dark_onTertiary = Color(0xFF243515)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
|
||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
val md_theme_dark_onError = Color(0xFF690005)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_dark_background = Color(0xFF1F1B16)
|
||||
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_surface = Color(0xFF1F1B16)
|
||||
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
|
||||
val md_theme_dark_outline = Color(0xFF9C8F80)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_inversePrimary = Color(0xFF825500)
|
||||
val md_theme_dark_shadow = Color(0xFF000000)
|
||||
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
|
||||
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
|
||||
val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
|
||||
val seed = Color(0xFF825500)
|
||||
|
||||
29
app/src/main/java/com/kouros/navigation/ui/theme/Shapes.kt
Normal 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)
|
||||
)
|
||||
@@ -1,57 +1,111 @@
|
||||
package com.kouros.navigation.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
private val LightColors = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
private val DarkColors = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NavigationTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
val context = LocalContext.current
|
||||
val colors = run {
|
||||
if (useDarkTheme) dynamicDarkColorScheme(context)
|
||||
else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colors.primary.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
|
||||
useDarkTheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = typography,
|
||||
content = content,
|
||||
shapes = shapes,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,28 +7,35 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
val typography = Typography(
|
||||
headlineSmall = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 18.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
letterSpacing = 0.15.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
bodyMedium = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ buildscript {
|
||||
val objectboxVersion by extra("5.0.1") // For KTS build scripts
|
||||
|
||||
dependencies {
|
||||
// Android Gradle Plugin 8.0 or later supported
|
||||
classpath(libs.gradle)
|
||||
classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion")
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.car.app.hardware.common.CarValue
|
||||
import androidx.car.app.hardware.common.OnCarDataAvailableListener
|
||||
import androidx.car.app.hardware.info.CarHardwareLocation
|
||||
import androidx.car.app.hardware.info.CarSensors
|
||||
import androidx.car.app.hardware.info.Compass
|
||||
import androidx.car.app.hardware.info.Speed
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.net.toUri
|
||||
@@ -29,19 +30,18 @@ import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.car.screen.NavigationScreen
|
||||
import com.kouros.navigation.car.screen.RequestPermissionScreen
|
||||
import com.kouros.navigation.car.screen.SearchScreen
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.Constants.CAR_LOCATION
|
||||
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
|
||||
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
|
||||
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||
import com.kouros.navigation.data.Constants.TAG
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.osrm.OsrmRepository
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||
import com.kouros.navigation.utils.NavigationUtils.getRouteEngine
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||
|
||||
class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
|
||||
@@ -54,33 +54,32 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
lateinit var surfaceRenderer: SurfaceRenderer
|
||||
|
||||
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
|
||||
val routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
|
||||
if (routingEngine == RouteEngine.VALHALLA.ordinal) {
|
||||
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
|
||||
if (!useCarLocation) {
|
||||
updateLocation(location!!)
|
||||
}
|
||||
}
|
||||
|
||||
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
Log.i(TAG, "In onCreate()")
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
Log.i(TAG, "In onResume()")
|
||||
}
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
Log.i(TAG, "In onPause()")
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
Log.i(TAG, "In onStop()")
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
|
||||
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
||||
carSensors.removeCarHardwareLocationListener(carLocationListener)
|
||||
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
|
||||
if (useCarLocation) {
|
||||
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
|
||||
carSensors.removeCarHardwareLocationListener(carLocationListener)
|
||||
}
|
||||
carInfo.removeSpeedListener(carSpeedListener)
|
||||
Log.i(TAG, "In onDestroy()")
|
||||
val locationManager =
|
||||
@@ -91,13 +90,23 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
|
||||
lateinit var navigationViewModel: ViewModel
|
||||
|
||||
lateinit var baseStyle: BaseStyle.Json
|
||||
|
||||
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
|
||||
OnCarDataAvailableListener { data ->
|
||||
if (data.location.status == CarValue.STATUS_SUCCESS) {
|
||||
val location = data.location.value
|
||||
surfaceRenderer.updateCarLocation(location!!)
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,21 +124,21 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
fun onRoutingEngineStateUpdated(routeEngine : Int) {
|
||||
navigationViewModel = when (routeEngine) {
|
||||
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
|
||||
else -> ViewModel(OsrmRepository())
|
||||
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
|
||||
else -> ViewModel(TomTomRepository())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
|
||||
navigationViewModel = getRouteEngine(carContext)
|
||||
navigationViewModel = getViewModel(carContext)
|
||||
|
||||
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
|
||||
|
||||
routeModel = RouteCarModel()
|
||||
|
||||
val darkMode = getIntKeyValue(carContext, Constants.DARK_MODE_SETTINGS)
|
||||
baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
||||
|
||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, baseStyle)
|
||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)
|
||||
|
||||
navigationScreen =
|
||||
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
|
||||
@@ -165,13 +174,19 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
}
|
||||
|
||||
fun addSensors() {
|
||||
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
|
||||
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
|
||||
carSensors.addCarHardwareLocationListener(
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
carContext.mainExecutor,
|
||||
carLocationListener
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -185,8 +200,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
SearchScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
location,
|
||||
navigationViewModel,
|
||||
navigationViewModel
|
||||
// TODO: Uri
|
||||
)
|
||||
) { obj: Any? ->
|
||||
@@ -223,11 +237,11 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
if (location != null) {
|
||||
navigationViewModel.loadRecentPlace(location = location, carContext)
|
||||
navigationViewModel.loadRecentPlace(location = location, surfaceRenderer.carOrientation, carContext)
|
||||
updateLocation(location)
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
/* minTimeMs= */ 1000,
|
||||
/* minTimeMs= */ 500,
|
||||
/* minDistanceM= */ 5f,
|
||||
mLocationListener
|
||||
)
|
||||
@@ -237,16 +251,18 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
fun updateLocation(location: Location) {
|
||||
if (routeModel.isNavigating()) {
|
||||
navigationScreen.updateTrip(location)
|
||||
val wayPointLocation = routeModel.route.currentStep().wayPointLocation
|
||||
val distance = location.distanceTo(wayPointLocation)
|
||||
if (distance > MAXIMAL_ROUTE_DEVIATION) {
|
||||
navigationScreen.calculateNewRoute(routeModel.routeState.destination)
|
||||
return
|
||||
}
|
||||
if (distance < MAXIMAL_SNAP_CORRECTION) {
|
||||
surfaceRenderer.updateLocation(wayPointLocation)
|
||||
} else {
|
||||
surfaceRenderer.updateLocation(location)
|
||||
if (!routeModel.navState.arrived) {
|
||||
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
||||
val distance = location.distanceTo(snapedLocation)
|
||||
if (distance > MAXIMAL_ROUTE_DEVIATION) {
|
||||
navigationScreen.calculateNewRoute(routeModel.navState.destination)
|
||||
return
|
||||
}
|
||||
if (distance < MAXIMAL_SNAP_CORRECTION) {
|
||||
surfaceRenderer.updateLocation(snapedLocation)
|
||||
} else {
|
||||
surfaceRenderer.updateLocation(location)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
surfaceRenderer.updateLocation(location)
|
||||
|
||||
@@ -29,10 +29,13 @@ import com.kouros.navigation.car.map.cameraState
|
||||
import com.kouros.navigation.car.map.getPaddingValues
|
||||
import com.kouros.navigation.car.map.rememberBaseStyle
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||
import com.kouros.navigation.data.Constants.homeLocation
|
||||
import com.kouros.navigation.data.Constants.homeVogelhart
|
||||
import com.kouros.navigation.data.ObjectBox
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.tomtom.TrafficData
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||
import com.kouros.navigation.utils.bearing
|
||||
@@ -49,15 +52,16 @@ import org.maplibre.spatialk.geojson.Position
|
||||
|
||||
class SurfaceRenderer(
|
||||
private var carContext: CarContext, lifecycle: Lifecycle,
|
||||
private var routeModel: RouteCarModel,
|
||||
private var baseStyle: BaseStyle.Json
|
||||
private var routeModel: RouteCarModel
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
var lastLocation = location(0.0, 0.0)
|
||||
|
||||
var carOrientation = 999F
|
||||
private val cameraPosition = MutableLiveData(
|
||||
CameraPosition(
|
||||
zoom = 15.0,
|
||||
target = Position(latitude = homeLocation.latitude, longitude = homeLocation.longitude)
|
||||
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
|
||||
)
|
||||
)
|
||||
private var visibleArea = MutableLiveData(
|
||||
@@ -68,14 +72,21 @@ class SurfaceRenderer(
|
||||
var height = 0
|
||||
var lastBearing = 0.0
|
||||
val routeData = MutableLiveData("")
|
||||
|
||||
val trafficData = MutableLiveData(emptyMap<String, String>())
|
||||
val speedCamerasData = MutableLiveData("")
|
||||
val speed = MutableLiveData(0F)
|
||||
lateinit var centerLocation: Location
|
||||
val maxSpeed = MutableLiveData(0)
|
||||
var viewStyle = ViewStyle.VIEW
|
||||
lateinit var centerLocation: Location
|
||||
var previewDistance = 0.0
|
||||
lateinit var mapView: ComposeView
|
||||
var tilt = 55.0
|
||||
var countDownTimerActive = false
|
||||
|
||||
val style: MutableLiveData<BaseStyle> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
|
||||
|
||||
lateinit var lifecycleOwner: CustomLifecycleOwner
|
||||
@@ -158,6 +169,7 @@ class SurfaceRenderer(
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
speed.value = 0F
|
||||
|
||||
}
|
||||
|
||||
fun onConnectionStateUpdated(connectionState: Int) {
|
||||
@@ -168,15 +180,24 @@ class SurfaceRenderer(
|
||||
}
|
||||
}
|
||||
|
||||
fun onBaseStyleStateUpdated(style: BaseStyle) {
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MapView() {
|
||||
|
||||
val darkMode = getIntKeyValue(carContext, Constants.DARK_MODE_SETTINGS)
|
||||
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
||||
|
||||
val position: CameraPosition? by cameraPosition.observeAsState()
|
||||
val route: String? by routeData.observeAsState()
|
||||
val traffic: Map<String, String> ? by trafficData.observeAsState()
|
||||
val speedCameras: String? by speedCamerasData.observeAsState()
|
||||
val paddingValues = getPaddingValues(height, viewStyle)
|
||||
val cameraState = cameraState(paddingValues, position, tilt)
|
||||
val rememberBaseStyle = rememberBaseStyle(baseStyle)
|
||||
MapLibre(carContext, cameraState, rememberBaseStyle, route, viewStyle, speedCameras)
|
||||
MapLibre(carContext, cameraState, rememberBaseStyle, route, traffic, viewStyle, speedCameras)
|
||||
ShowPosition(cameraState, position, paddingValues)
|
||||
}
|
||||
|
||||
@@ -189,11 +210,12 @@ class SurfaceRenderer(
|
||||
val cameraDuration =
|
||||
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
|
||||
val currentSpeed: Float? by speed.observeAsState()
|
||||
val maxSpeed: Int? by maxSpeed.observeAsState()
|
||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||
DrawNavigationImages(
|
||||
paddingValues,
|
||||
currentSpeed,
|
||||
routeModel.routeState.maxSpeed,
|
||||
maxSpeed!!,
|
||||
width,
|
||||
height
|
||||
)
|
||||
@@ -214,6 +236,7 @@ class SurfaceRenderer(
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
CarConnection(carContext).type.observe(owner, ::onConnectionStateUpdated)
|
||||
style.observe(owner, :: onBaseStyleStateUpdated)
|
||||
Log.i(TAG, "SurfaceRenderer created")
|
||||
carContext.getCarService(AppManager::class.java)
|
||||
.setSurfaceCallback(mSurfaceCallback)
|
||||
@@ -242,11 +265,14 @@ class SurfaceRenderer(
|
||||
fun updateLocation(location: Location) {
|
||||
synchronized(this) {
|
||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||
val bearing = bearing(
|
||||
val bearing = if (carOrientation == 999F)
|
||||
bearing(
|
||||
lastLocation,
|
||||
location,
|
||||
cameraPosition.value!!.bearing
|
||||
)
|
||||
) else {
|
||||
carOrientation.toDouble()
|
||||
}
|
||||
val zoom = if (viewStyle == ViewStyle.VIEW) {
|
||||
calculateZoom(location.speed.toDouble())
|
||||
} else {
|
||||
@@ -278,16 +304,20 @@ class SurfaceRenderer(
|
||||
}
|
||||
|
||||
fun setRouteData() {
|
||||
routeData.value = routeModel.route.routeGeoJson
|
||||
routeData.value = routeModel.curRoute.routeGeoJson
|
||||
viewStyle = ViewStyle.VIEW
|
||||
}
|
||||
|
||||
fun setTrafficData(traffic: Map<String, String> ) {
|
||||
trafficData.value = traffic as MutableMap<String, String>?
|
||||
}
|
||||
|
||||
fun setPreviewRouteData(routeModel: RouteModel) {
|
||||
viewStyle = ViewStyle.PREVIEW
|
||||
with(routeModel) {
|
||||
routeData.value = route.routeGeoJson
|
||||
centerLocation = route.centerLocation
|
||||
previewDistance = route.summary!!.distance
|
||||
routeData.value = curRoute.routeGeoJson
|
||||
centerLocation = curRoute.centerLocation
|
||||
previewDistance = curRoute.summary.distance
|
||||
}
|
||||
updateCameraPosition(
|
||||
0.0,
|
||||
@@ -302,7 +332,7 @@ class SurfaceRenderer(
|
||||
routeData.value = route
|
||||
updateCameraPosition(
|
||||
0.0,
|
||||
12.0,
|
||||
14.0,
|
||||
target = Position(location.longitude, location.latitude)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,18 +10,15 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.scale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.drawText
|
||||
@@ -36,17 +33,18 @@ import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
|
||||
import com.kouros.navigation.data.NavigationColor
|
||||
import com.kouros.navigation.data.RouteColor
|
||||
import com.kouros.navigation.data.SpeedColor
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
import org.maplibre.compose.expressions.ast.Expression
|
||||
import org.maplibre.compose.expressions.dsl.const
|
||||
import org.maplibre.compose.expressions.dsl.exponential
|
||||
import org.maplibre.compose.expressions.dsl.image
|
||||
import org.maplibre.compose.expressions.dsl.interpolate
|
||||
import org.maplibre.compose.expressions.dsl.zoom
|
||||
import org.maplibre.compose.expressions.value.ColorValue
|
||||
import org.maplibre.compose.layers.Anchor
|
||||
import org.maplibre.compose.layers.FillLayer
|
||||
import org.maplibre.compose.layers.LineLayer
|
||||
@@ -92,10 +90,10 @@ fun MapLibre(
|
||||
cameraState: CameraState,
|
||||
baseStyle: BaseStyle.Json,
|
||||
route: String?,
|
||||
traffic: Map<String, String>?,
|
||||
viewStyle: ViewStyle,
|
||||
speedCameras: String? = ""
|
||||
) {
|
||||
|
||||
MaplibreMap(
|
||||
options = MapOptions(
|
||||
ornamentOptions =
|
||||
@@ -111,7 +109,8 @@ fun MapLibre(
|
||||
if (viewStyle == ViewStyle.AMENITY_VIEW) {
|
||||
AmenityLayer(route)
|
||||
} else {
|
||||
RouteLayer(route)
|
||||
RouteLayer(route, traffic!!)
|
||||
//RouteLayerPoint(route )
|
||||
}
|
||||
SpeedCameraLayer(speedCameras)
|
||||
}
|
||||
@@ -121,7 +120,7 @@ fun MapLibre(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RouteLayer(routeData: String?) {
|
||||
fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
|
||||
if (routeData != null && routeData.isNotEmpty()) {
|
||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||
LineLayer(
|
||||
@@ -153,23 +152,105 @@ fun RouteLayer(routeData: String?) {
|
||||
),
|
||||
)
|
||||
}
|
||||
trafficData.forEach {
|
||||
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
|
||||
LineLayer(
|
||||
id = "traffic-${it.key}-casing",
|
||||
source = traffic,
|
||||
color = const(Color.White),
|
||||
width =
|
||||
interpolate(
|
||||
type = exponential(1.2f),
|
||||
input = zoom(),
|
||||
5 to const(0.4.dp),
|
||||
6 to const(0.6.dp),
|
||||
7 to const(1.8.dp),
|
||||
20 to const(20.dp),
|
||||
),
|
||||
)
|
||||
LineLayer(
|
||||
id = "traffic-${it.key}",
|
||||
source = traffic,
|
||||
color = trafficColor(it.key),
|
||||
width =
|
||||
interpolate(
|
||||
type = exponential(1.2f),
|
||||
input = zoom(),
|
||||
5 to const(0.4.dp),
|
||||
6 to const(0.5.dp),
|
||||
7 to const(1.6.dp),
|
||||
20 to const(18.dp),
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun RouteLayerPoint(routeData: String?) {
|
||||
if (routeData != null && routeData.isNotEmpty()) {
|
||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||
val img = image(painterResource(R.drawable.ic_favorite_filled_white_24dp), drawAsSdf = true)
|
||||
SymbolLayer(
|
||||
id = "point-layer",
|
||||
source = routes,
|
||||
iconOpacity = const(2.0f),
|
||||
iconColor = const(Color.Red),
|
||||
iconImage = img,
|
||||
iconSize =
|
||||
interpolate(
|
||||
type = exponential(1.2f),
|
||||
input = zoom(),
|
||||
5 to const(0.4f),
|
||||
6 to const(0.6f),
|
||||
7 to const(0.8f),
|
||||
20 to const(1.0f),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun trafficColor(key: String): Expression<ColorValue> {
|
||||
when (key) {
|
||||
"queuing" -> return const(Color(0xFFD24417))
|
||||
"stationary" -> return const(Color(0xFFFF0000))
|
||||
"heavy" -> return const(Color(0xFF6B0404))
|
||||
"slow" -> return const(Color(0xFFC41F1F))
|
||||
"roadworks" -> return const(Color(0xFF7A631A))
|
||||
}
|
||||
return const(Color.Blue)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AmenityLayer(routeData: String?) {
|
||||
if (routeData != null && routeData.isNotEmpty()) {
|
||||
val color = if (routeData.contains(Constants.PHARMACY)) {
|
||||
const(Color.Red)
|
||||
} else {
|
||||
const(Color.Green)
|
||||
var color = const(Color.Red)
|
||||
var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
|
||||
if (routeData.contains(Constants.CHARGING_STATION)) {
|
||||
color = const(Color.Green)
|
||||
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
|
||||
} else if (routeData.contains(Constants.FUEL_STATION)) {
|
||||
color = const(Color.Black)
|
||||
img = image(painterResource(R.drawable.local_gas_station_48px), drawAsSdf = true)
|
||||
}
|
||||
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
|
||||
SymbolLayer(
|
||||
id = "amenity-layer",
|
||||
source = routes,
|
||||
iconImage = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true),
|
||||
iconImage = img,
|
||||
iconColor = color,
|
||||
iconSize = const(3.0f),
|
||||
iconOpacity = const(2.0f),
|
||||
iconSize =
|
||||
interpolate(
|
||||
type = exponential(1.2f),
|
||||
input = zoom(),
|
||||
5 to const(0.7f),
|
||||
6 to const(1.0f),
|
||||
7 to const(2.0f),
|
||||
20 to const(4f),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -188,10 +269,10 @@ fun SpeedCameraLayer(speedCameras: String?) {
|
||||
interpolate(
|
||||
type = exponential(1.2f),
|
||||
input = zoom(),
|
||||
5 to const(0.4f),
|
||||
6 to const(0.7f),
|
||||
7 to const(1.75f),
|
||||
20 to const(3f),
|
||||
5 to const(0.7f),
|
||||
6 to const(1.0f),
|
||||
7 to const(2.0f),
|
||||
20 to const(4f),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -224,6 +305,7 @@ fun DrawNavigationImages(
|
||||
if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) {
|
||||
MaxSpeed(width, height, maxSpeed)
|
||||
}
|
||||
//DebugInfo(width, height, routeModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -257,7 +339,7 @@ private fun CurrentSpeed(
|
||||
curSpeed: Float,
|
||||
maxSpeed: Int
|
||||
) {
|
||||
val radius = 32
|
||||
val radius = 34
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
@@ -323,7 +405,7 @@ private fun MaxSpeed(
|
||||
height: Int,
|
||||
maxSpeed: Int,
|
||||
) {
|
||||
val radius = 20
|
||||
val radius = 24
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
@@ -373,14 +455,52 @@ private fun MaxSpeed(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBaseStyle( baseStyle : BaseStyle.Json): BaseStyle.Json {
|
||||
fun DebugInfo(
|
||||
width: Int,
|
||||
height: Int,
|
||||
routeModel: RouteModel,
|
||||
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 20.dp,
|
||||
top = 0.dp
|
||||
),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
val textMeasurerLocation = rememberTextMeasurer()
|
||||
val location = routeModel.navState.currentLocation.latitude.toString()
|
||||
val styleSpeed = TextStyle(
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
)
|
||||
val textLayoutLocation = remember(location) {
|
||||
textMeasurerLocation.measure(location, styleSpeed)
|
||||
}
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawText(
|
||||
textMeasurer = textMeasurerLocation,
|
||||
text = location,
|
||||
style = styleSpeed,
|
||||
topLeft = Offset(
|
||||
x = center.x - textLayoutLocation.size.width / 2,
|
||||
y = center.y - textLayoutLocation.size.height / 2,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBaseStyle(baseStyle: BaseStyle.Json): BaseStyle.Json {
|
||||
val rememberBaseStyle by remember() {
|
||||
mutableStateOf(baseStyle)
|
||||
}
|
||||
return rememberBaseStyle
|
||||
}
|
||||
|
||||
|
||||
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
|
||||
return when (viewStyle) {
|
||||
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.kouros.navigation.car.navigation
|
||||
|
||||
import android.location.Location
|
||||
import android.text.SpannableString
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
@@ -32,11 +33,17 @@ import androidx.car.app.model.Distance
|
||||
import androidx.car.app.navigation.model.Lane
|
||||
import androidx.car.app.navigation.model.LaneDirection
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
||||
import androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|
||||
import androidx.car.app.navigation.model.Step
|
||||
import androidx.car.app.navigation.model.TravelEstimate
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.data.StepData
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.location
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -48,23 +55,24 @@ class RouteCarModel() : RouteModel() {
|
||||
val stepData = currentStep()
|
||||
val currentStepCueWithImage: SpannableString =
|
||||
createString(stepData.instruction)
|
||||
val straightNormal =
|
||||
Lane.Builder()
|
||||
.addDirection(LaneDirection.create(LaneDirection.SHAPE_STRAIGHT, false))
|
||||
.build()
|
||||
|
||||
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
|
||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|
||||
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
||||
) {
|
||||
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
|
||||
}
|
||||
val step =
|
||||
Step.Builder(currentStepCueWithImage)
|
||||
.setManeuver(
|
||||
Maneuver.Builder(stepData.maneuverType)
|
||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||
.build()
|
||||
maneuver.build()
|
||||
)
|
||||
.setRoad(routeState.destination.street!!)
|
||||
stepData.lane.forEach {
|
||||
if (it.indications.isNotEmpty() ) {
|
||||
step.setLanesImage(createCarIcon(createLaneIcon(carContext, stepData)))
|
||||
step.addLane(straightNormal)
|
||||
}
|
||||
if (navState.destination.street != null) {
|
||||
step.setRoad(navState.destination.street!!)
|
||||
}
|
||||
if (stepData.lane.isNotEmpty()) {
|
||||
addLanes(carContext, step, stepData)
|
||||
}
|
||||
return step.build()
|
||||
}
|
||||
@@ -74,29 +82,34 @@ class RouteCarModel() : RouteModel() {
|
||||
val stepData = nextStep()
|
||||
val currentStepCueWithImage: SpannableString =
|
||||
createString(stepData.instruction)
|
||||
val maneuver = Maneuver.Builder(stepData.currentManeuverType)
|
||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||
if (stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW
|
||||
|| stepData.currentManeuverType == TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
||||
) {
|
||||
maneuver.setRoundaboutExitNumber(stepData.exitNumber)
|
||||
}
|
||||
val step =
|
||||
Step.Builder(currentStepCueWithImage)
|
||||
.setManeuver(
|
||||
Maneuver.Builder(stepData.maneuverType)
|
||||
.setIcon(createCarIcon(carContext, stepData.icon))
|
||||
.build()
|
||||
maneuver.build()
|
||||
)
|
||||
.build()
|
||||
return step
|
||||
}
|
||||
|
||||
fun travelEstimate(carContext: CarContext): TravelEstimate {
|
||||
val timeLeft = travelLeftTime()
|
||||
val timeLeft = routeCalculator.travelLeftTime()
|
||||
val timeToDestinationMillis =
|
||||
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
||||
val leftDistance = travelLeftDistance()
|
||||
val leftDistance = routeCalculator.travelLeftDistance() / 1000
|
||||
val displayUnit = if (leftDistance > 1.0) {
|
||||
Distance.UNIT_KILOMETERS
|
||||
} else {
|
||||
Distance.UNIT_METERS
|
||||
}
|
||||
val arivalTime = DateTimeWithZone.create(
|
||||
arrivalTime(),
|
||||
val arrivalTime = DateTimeWithZone.create(
|
||||
routeCalculator.arrivalTime(),
|
||||
TimeZone.getTimeZone("Europe/Berlin")
|
||||
)
|
||||
val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination.
|
||||
@@ -104,23 +117,52 @@ class RouteCarModel() : RouteModel() {
|
||||
leftDistance,
|
||||
displayUnit
|
||||
), // Arrival time at the destination with the destination time zone.
|
||||
arivalTime
|
||||
arrivalTime
|
||||
)
|
||||
.setRemainingTimeSeconds(
|
||||
TimeUnit.MILLISECONDS.toSeconds(
|
||||
timeToDestinationMillis
|
||||
)
|
||||
)
|
||||
.setRemainingTimeColor(CarColor.YELLOW)
|
||||
.setRemainingDistanceColor(CarColor.RED)
|
||||
.setRemainingTimeColor(CarColor.GREEN)
|
||||
.setRemainingDistanceColor(CarColor.BLUE)
|
||||
|
||||
if (routeState.travelMessage.isNotEmpty()) {
|
||||
if (navState.travelMessage.isNotEmpty()) {
|
||||
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
|
||||
travelBuilder.setTripText(CarText.create(routeState.travelMessage))
|
||||
travelBuilder.setTripText(CarText.create(navState.travelMessage))
|
||||
}
|
||||
return travelBuilder.build()
|
||||
}
|
||||
|
||||
fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) {
|
||||
var laneImageAdded = false
|
||||
stepData.lane.forEach {
|
||||
if (it.indications.isNotEmpty() && it.valid) {
|
||||
Collections.sort<String>(it.indications)
|
||||
var direction = ""
|
||||
it.indications.forEach { it2 ->
|
||||
direction = if (direction.isEmpty()) {
|
||||
it2.trim()
|
||||
} else {
|
||||
"${direction}_${it2.trim()}"
|
||||
}
|
||||
}
|
||||
val laneDirection = navState.iconMapper.addLanes(direction, stepData)
|
||||
if (laneDirection != LaneDirection.SHAPE_UNKNOWN) {
|
||||
if (!laneImageAdded) {
|
||||
step.setLanesImage(createCarIcon(navState.iconMapper.createLaneIcon(carContext, stepData)))
|
||||
laneImageAdded = true
|
||||
}
|
||||
val laneType =
|
||||
Lane.Builder()
|
||||
.addDirection(LaneDirection.create(laneDirection, false))
|
||||
.build()
|
||||
step.addLane(laneType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createString(
|
||||
text: String
|
||||
): SpannableString {
|
||||
@@ -136,27 +178,35 @@ class RouteCarModel() : RouteModel() {
|
||||
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
|
||||
}
|
||||
|
||||
fun createCarIcon(iconCompat: IconCompat): CarIcon {
|
||||
fun createCarIcon(iconCompat: IconCompat): CarIcon {
|
||||
return CarIcon.Builder(iconCompat).build()
|
||||
}
|
||||
|
||||
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String?) {
|
||||
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) {
|
||||
carContext.getCarService<AppManager?>(AppManager::class.java)
|
||||
.showAlert(createAlert(carContext, distance, maxSpeed))
|
||||
.showAlert(
|
||||
createAlert(
|
||||
carContext,
|
||||
maxSpeed,
|
||||
createCarIcon(carContext, R.drawable.speed_camera_24px)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun createAlert(carContext: CarContext, distance: Double, maxSpeed: String?): Alert {
|
||||
fun createAlert(
|
||||
carContext: CarContext,
|
||||
maxSpeed: String?,
|
||||
icon: CarIcon
|
||||
): Alert {
|
||||
val title = createCarText(carContext, R.string.speed_camera)
|
||||
val subtitle = CarText.create(maxSpeed!!)
|
||||
val icon = CarIcon.ALERT
|
||||
|
||||
val dismissAction: Action = createToastAction(
|
||||
carContext,
|
||||
R.string.exit_action_title, R.string.exit_action_title,
|
||||
FLAG_DEFAULT
|
||||
)
|
||||
|
||||
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */10000)
|
||||
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */5000)
|
||||
.setSubtitle(subtitle)
|
||||
.setIcon(icon)
|
||||
.addAction(dismissAction).setCallback(object : AlertCallback {
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.core.graphics.drawable.IconCompat
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.ViewStyle
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.data.Category
|
||||
import com.kouros.navigation.data.Constants.CHARGING_STATION
|
||||
import com.kouros.navigation.data.Constants.FUEL_STATION
|
||||
@@ -23,8 +24,7 @@ import com.kouros.navigation.model.ViewModel
|
||||
class CategoriesScreen(
|
||||
private val carContext: CarContext,
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
private val location: Location,
|
||||
private val viewModel: ViewModel
|
||||
private val viewModel: ViewModel,
|
||||
) : Screen(carContext) {
|
||||
|
||||
var categories: List<Category> = listOf(
|
||||
@@ -47,7 +47,6 @@ class CategoriesScreen(
|
||||
CategoryScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
location,
|
||||
it.id,
|
||||
viewModel
|
||||
)
|
||||
|
||||
@@ -19,8 +19,9 @@ import androidx.lifecycle.Observer
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.navigation.NavigationMessage
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
||||
@@ -31,9 +32,8 @@ import kotlin.math.min
|
||||
class CategoryScreen(
|
||||
private val carContext: CarContext,
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
location: Location,
|
||||
private val category: String,
|
||||
private val viewModel: ViewModel
|
||||
private val viewModel: ViewModel,
|
||||
) : Screen(carContext) {
|
||||
|
||||
var elements = listOf<Elements>()
|
||||
@@ -44,10 +44,10 @@ class CategoryScreen(
|
||||
val loc = location(0.0, 0.0)
|
||||
elements.forEach {
|
||||
if (loc.latitude == 0.0) {
|
||||
loc.longitude = it.lon!!
|
||||
loc.latitude = it.lat!!
|
||||
loc.longitude = it.lon
|
||||
loc.latitude = it.lat
|
||||
}
|
||||
coordinates.add(listOf(it.lon!!, it.lat!!))
|
||||
coordinates.add(listOf(it.lon, it.lat))
|
||||
}
|
||||
if (elements.isNotEmpty()) {
|
||||
val route = createPointCollection(coordinates, category)
|
||||
@@ -58,7 +58,7 @@ class CategoryScreen(
|
||||
|
||||
init {
|
||||
viewModel.elements.observe(this, observer)
|
||||
viewModel.getAmenities(category, location)
|
||||
viewModel.getAmenities(category, surfaceRenderer.lastLocation)
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ class CategoryScreen(
|
||||
}
|
||||
val row = Row.Builder()
|
||||
.setOnClickListener {
|
||||
val location = location(it.lon!!, it.lat!!)
|
||||
val location = location(it.lon, it.lat)
|
||||
surfaceRenderer.setCategoryLocation(location, category)
|
||||
}
|
||||
.setTitle(name)
|
||||
@@ -126,6 +126,28 @@ class CategoryScreen(
|
||||
} else {
|
||||
row.addText(carText("${it.tags.openingHours}"))
|
||||
}
|
||||
val navigationMessage = NavigationMessage(carContext)
|
||||
row.addAction(
|
||||
Action.Builder()
|
||||
.setOnClickListener {
|
||||
viewModel.loadRoute(
|
||||
carContext,
|
||||
currentLocation = surfaceRenderer.lastLocation,
|
||||
location(it.lon!!, it.lat!!),
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
setResult(
|
||||
Place(
|
||||
name = name,
|
||||
category = Constants.CHARGING_STATION,
|
||||
latitude = it.lat!!,
|
||||
longitude = it.lon!!
|
||||
)
|
||||
)
|
||||
finish()
|
||||
}
|
||||
.setIcon(navigationMessage.createCarIcon(R.drawable.navigation_48px))
|
||||
.build())
|
||||
return row.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
import com.kouros.navigation.utils.GeoUtils
|
||||
import com.kouros.navigation.utils.location
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class NavigationScreen(
|
||||
@@ -55,6 +57,7 @@ class NavigationScreen(
|
||||
var recentPlace = Place()
|
||||
var navigationType = NavigationType.VIEW
|
||||
|
||||
var lastTrafficDate = LocalDateTime.of(1960, 6, 21, 0, 0)
|
||||
val observer = Observer<String> { route ->
|
||||
if (route.isNotEmpty()) {
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
@@ -71,6 +74,10 @@ class NavigationScreen(
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
val trafficObserver = Observer<Map<String, String> > { traffic ->
|
||||
surfaceRenderer.setTrafficData(traffic)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
val placeObserver = Observer<SearchResult> { searchResult ->
|
||||
val place = Place(
|
||||
@@ -98,8 +105,10 @@ class NavigationScreen(
|
||||
surfaceRenderer.speedCamerasData.value = speedData
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
viewModel.route.observe(this, observer)
|
||||
viewModel.traffic.observe(this, trafficObserver);
|
||||
viewModel.recentPlace.observe(this, recentObserver)
|
||||
viewModel.placeLocation.observe(this, placeObserver)
|
||||
viewModel.speedCameras.observe(this, speedObserver)
|
||||
@@ -141,11 +150,11 @@ class NavigationScreen(
|
||||
}
|
||||
|
||||
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
||||
if (routeModel.routeState.arrived) {
|
||||
if (routeModel.navState.arrived) {
|
||||
val timer = object : CountDownTimer(8000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
routeModel.routeState = routeModel.routeState.copy(arrived = false)
|
||||
routeModel.navState = routeModel.navState.copy(arrived = false)
|
||||
navigationType = NavigationType.VIEW
|
||||
invalidate()
|
||||
}
|
||||
@@ -164,8 +173,8 @@ class NavigationScreen(
|
||||
|
||||
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||
var street = ""
|
||||
if (routeModel.routeState.destination.street != null) {
|
||||
street = routeModel.routeState.destination.street!!
|
||||
if (routeModel.navState.destination.street != null) {
|
||||
street = routeModel.navState.destination.street!!
|
||||
}
|
||||
return NavigationTemplate.Builder()
|
||||
.setNavigationInfo(
|
||||
@@ -224,7 +233,7 @@ class NavigationScreen(
|
||||
}
|
||||
|
||||
fun getRoutingInfo(): RoutingInfo {
|
||||
var currentDistance = routeModel.leftStepDistance()
|
||||
var currentDistance = routeModel.routeCalculator.leftStepDistance()
|
||||
val displayUnit = if (currentDistance > 1000.0) {
|
||||
currentDistance /= 1000.0
|
||||
Distance.UNIT_KILOMETERS
|
||||
@@ -297,8 +306,13 @@ class NavigationScreen(
|
||||
)
|
||||
.setOnClickListener {
|
||||
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
|
||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo)
|
||||
routeModel.routeState = routeModel.routeState.copy(destination = recentPlace)
|
||||
viewModel.loadRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
navigateTo,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
routeModel.navState = routeModel.navState.copy(destination = recentPlace)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
@@ -394,7 +408,11 @@ class NavigationScreen(
|
||||
private fun startSearchScreen() {
|
||||
screenManager
|
||||
.pushForResult(
|
||||
SearchScreen(carContext, surfaceRenderer, surfaceRenderer.lastLocation, viewModel)
|
||||
SearchScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
viewModel
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
val place = obj as Place
|
||||
@@ -416,8 +434,13 @@ class NavigationScreen(
|
||||
val location = location(place.longitude, place.latitude)
|
||||
viewModel.saveRecent(place)
|
||||
currentNavigationLocation = location
|
||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
|
||||
routeModel.routeState = routeModel.routeState.copy(destination = place)
|
||||
viewModel.loadRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
location,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
routeModel.navState = routeModel.navState.copy(destination = place)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@@ -435,7 +458,7 @@ class NavigationScreen(
|
||||
invalidate()
|
||||
val mainThreadHandler = Handler(carContext.mainLooper)
|
||||
mainThreadHandler.post {
|
||||
object : CountDownTimer(3000, 1000) {
|
||||
object : CountDownTimer(2000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
@@ -447,18 +470,32 @@ class NavigationScreen(
|
||||
|
||||
fun reRoute(destination: Place) {
|
||||
val dest = location(destination.longitude, destination.latitude)
|
||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, dest)
|
||||
viewModel.loadRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
dest,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
|
||||
fun updateTrip(location: Location) {
|
||||
updateSpeedCamera(surfaceRenderer.lastLocation)
|
||||
val current = LocalDateTime.now(ZoneOffset.UTC)
|
||||
val duration = java.time.Duration.between(current, lastTrafficDate)
|
||||
if (duration.abs().seconds > 360) {
|
||||
lastTrafficDate = current
|
||||
viewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
|
||||
}
|
||||
updateSpeedCamera(location)
|
||||
with(routeModel) {
|
||||
updateLocation(location, viewModel)
|
||||
if (routeState.maneuverType == Maneuver.TYPE_DESTINATION
|
||||
&& leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
|
||||
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||
) {
|
||||
stopNavigation()
|
||||
routeState = routeState.copy(arrived = true)
|
||||
navState = navState.copy(arrived = true)
|
||||
surfaceRenderer.routeData.value = ""
|
||||
navigationType = NavigationType.ARRIVAL
|
||||
invalidate()
|
||||
@@ -482,20 +519,27 @@ class NavigationScreen(
|
||||
val updatedCameras = mutableListOf<Elements>()
|
||||
speedCameras.forEach {
|
||||
val plLocation =
|
||||
location(longitude = it.lon!!, latitude = it.lat!!)
|
||||
location(longitude = it.lon, latitude = it.lat)
|
||||
val distance = plLocation.distanceTo(location)
|
||||
it.distance = distance.toDouble()
|
||||
updatedCameras.add(it)
|
||||
}
|
||||
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
|
||||
val camera = sortedList.first()
|
||||
val bearingSpeedCamera = location.bearingTo(location(camera.lon!!, camera.lat!!))
|
||||
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
|
||||
|
||||
if (camera.distance < 80
|
||||
&& (bearingSpeedCamera.absoluteValue - bearingRoute.absoluteValue).absoluteValue < 15.0
|
||||
) {
|
||||
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
|
||||
val bearingSpeedCamera = if (camera.tags.direction != null) {
|
||||
try {
|
||||
camera.tags.direction!!.toFloat()
|
||||
} catch ( e: Exception) {
|
||||
0F
|
||||
}
|
||||
} else {
|
||||
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
|
||||
}
|
||||
if (camera.distance < 80) {
|
||||
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
|
||||
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,21 +12,29 @@ import androidx.car.app.model.Toggle
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
|
||||
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY
|
||||
import com.kouros.navigation.data.Constants.CAR_LOCATION
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
|
||||
|
||||
|
||||
class NavigationSettings(private val carContext: CarContext, private var viewModel: ViewModel) : Screen(carContext) {
|
||||
class NavigationSettings(private val carContext: CarContext, private var viewModel: ViewModel) :
|
||||
Screen(carContext) {
|
||||
|
||||
private var motorWayToggleState = false
|
||||
|
||||
private var tollWayToggleState = false
|
||||
|
||||
private var carLocationToggleState = false
|
||||
|
||||
|
||||
init {
|
||||
motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
|
||||
|
||||
tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
|
||||
|
||||
carLocationToggleState = getBooleanKeyValue(carContext, CAR_LOCATION)
|
||||
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -53,6 +61,24 @@ class NavigationSettings(private val carContext: CarContext, private var viewMod
|
||||
tollWayToggleState = !tollWayToggleState
|
||||
}.setChecked(tollWayToggleState).build()
|
||||
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
|
||||
|
||||
val carLocationToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
if (checked) {
|
||||
setBooleanKeyValue(carContext, true, CAR_LOCATION)
|
||||
} else {
|
||||
setBooleanKeyValue(carContext, false, CAR_LOCATION)
|
||||
}
|
||||
carLocationToggleState = !carLocationToggleState
|
||||
}.setChecked(carLocationToggleState).build()
|
||||
|
||||
listBuilder.addItem(
|
||||
buildRowForTemplate(
|
||||
R.string.use_car_location,
|
||||
carLocationToggle
|
||||
)
|
||||
)
|
||||
|
||||
listBuilder.addItem(
|
||||
buildRowForScreenTemplate(
|
||||
RoutingSettings(carContext, viewModel),
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.text.SpannableString
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.Distance
|
||||
@@ -22,21 +21,16 @@ import androidx.lifecycle.Observer
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.Constants.CONTACTS
|
||||
import com.kouros.navigation.data.Constants.FAVORITES
|
||||
import com.kouros.navigation.data.Constants.RECENT
|
||||
import com.kouros.navigation.data.Constants.categories
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
class PlaceListScreen(
|
||||
private val carContext: CarContext,
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
private val location: Location,
|
||||
private val category: String,
|
||||
private val viewModel: ViewModel
|
||||
) : Screen(carContext) {
|
||||
@@ -68,13 +62,21 @@ class PlaceListScreen(
|
||||
|
||||
fun loadPlaces() {
|
||||
if (category == RECENT) {
|
||||
viewModel.loadRecentPlaces(carContext, location)
|
||||
viewModel.loadRecentPlaces(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
if (category == CONTACTS) {
|
||||
viewModel.loadContacts(carContext)
|
||||
}
|
||||
if (category == FAVORITES) {
|
||||
viewModel.loadFavorites(carContext, location)
|
||||
viewModel.loadFavorites(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +84,14 @@ class PlaceListScreen(
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
.setNoItemsMessage(carContext.getString(R.string.no_places))
|
||||
places.forEach {
|
||||
val street = if (it.street != null) {
|
||||
it.street
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val row = Row.Builder()
|
||||
.setImage(contactIcon(it.avatar, it.category))
|
||||
.setTitle("${it.street!!} ${it.city}")
|
||||
.setTitle("$street ${it.city}")
|
||||
.setOnClickListener {
|
||||
val place = Place(
|
||||
0,
|
||||
@@ -117,7 +124,7 @@ class PlaceListScreen(
|
||||
setSpan(
|
||||
DistanceSpan.create(
|
||||
Distance.create(
|
||||
it.distance.toDouble(),
|
||||
(it.distance/1000).toDouble(),
|
||||
Distance.UNIT_KILOMETERS
|
||||
)
|
||||
), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
|
||||
|
||||
@@ -6,11 +6,10 @@ import androidx.annotation.DrawableRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.Action.FLAG_DEFAULT
|
||||
import androidx.car.app.model.Action.FLAG_PRIMARY
|
||||
import androidx.car.app.model.ActionStrip
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.CarText
|
||||
import androidx.car.app.model.DurationSpan
|
||||
@@ -18,19 +17,17 @@ import androidx.car.app.model.Header
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
import androidx.car.app.model.OnClickListener
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.navigation.model.MapController
|
||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
import androidx.car.app.navigation.model.NavigationTemplate
|
||||
import androidx.car.app.navigation.model.RoutingInfo
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.navigation.NavigationMessage
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
import com.kouros.navigation.utils.location
|
||||
@@ -61,7 +58,12 @@ class RoutePreviewScreen(
|
||||
init {
|
||||
viewModel.previewRoute.observe(this, observer)
|
||||
val location = location(destination.longitude, destination.latitude)
|
||||
viewModel.loadPreviewRoute(carContext, surfaceRenderer.lastLocation, location)
|
||||
viewModel.loadPreviewRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
location,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -74,9 +76,14 @@ class RoutePreviewScreen(
|
||||
.setFlags(FLAG_DEFAULT)
|
||||
.setIcon(navigateActionIcon)
|
||||
.setOnClickListener { this.onNavigate() }
|
||||
|
||||
.build()
|
||||
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
var i = 0
|
||||
routeModel.route.routes.forEach { it ->
|
||||
itemListBuilder.addItem(createRow(i++, navigateAction))
|
||||
}
|
||||
|
||||
val header = Header.Builder()
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.setTitle(carContext.getString(R.string.route_preview))
|
||||
@@ -88,30 +95,40 @@ class RoutePreviewScreen(
|
||||
)
|
||||
.build()
|
||||
|
||||
val message = if (routeModel.isNavigating() && routeModel.route.waypoints!!.isNotEmpty()) {
|
||||
createRouteText()
|
||||
} else {
|
||||
CarText.Builder("Wait")
|
||||
.build()
|
||||
}
|
||||
val messageTemplate = MessageTemplate.Builder(
|
||||
message
|
||||
)
|
||||
.setHeader(header)
|
||||
.addAction(navigateAction)
|
||||
.setLoading(message.toString() == "Wait")
|
||||
.build()
|
||||
|
||||
val timer = object : CountDownTimer(5000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
//onNavigate()
|
||||
val message =
|
||||
if (routeModel.isNavigating() && routeModel.curRoute.waypoints!!.isNotEmpty()) {
|
||||
createRouteText(0)
|
||||
} else {
|
||||
CarText.Builder("Wait")
|
||||
.build()
|
||||
}
|
||||
if (routeModel.route.routes.size == 1) {
|
||||
val timer = object : CountDownTimer(5000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
onNavigate()
|
||||
}
|
||||
}
|
||||
timer.start()
|
||||
}
|
||||
timer.start()
|
||||
|
||||
val content = if (routeModel.route.routes.size > 1) {
|
||||
ListTemplate.Builder()
|
||||
.setHeader(header)
|
||||
.setSingleList(itemListBuilder.build())
|
||||
.build()
|
||||
} else {
|
||||
MessageTemplate.Builder(
|
||||
message
|
||||
)
|
||||
.setHeader(header)
|
||||
.addAction(navigateAction)
|
||||
.setLoading(message.toString() == "Wait")
|
||||
.build()
|
||||
|
||||
}
|
||||
return MapWithContentTemplate.Builder()
|
||||
.setContentTemplate(messageTemplate)
|
||||
.setContentTemplate(content)
|
||||
.setMapController(
|
||||
MapController.Builder().setMapActionStrip(
|
||||
getMapActionStrip()
|
||||
@@ -171,9 +188,13 @@ class RoutePreviewScreen(
|
||||
)
|
||||
.build()
|
||||
|
||||
private fun createRouteText(): CarText {
|
||||
val time = routeModel.route.summary!!.duration
|
||||
val length = BigDecimal(routeModel.route.summary!!.distance).setScale(1, RoundingMode.HALF_EVEN)
|
||||
private fun createRouteText(index: Int): CarText {
|
||||
val time = routeModel.route.routes[index].summary.duration
|
||||
val length =
|
||||
BigDecimal(routeModel.route.routes[index].summary.distance).setScale(
|
||||
1,
|
||||
RoundingMode.HALF_EVEN
|
||||
)
|
||||
val firstRoute = SpannableString(" \u00b7 $length km")
|
||||
firstRoute.setSpan(
|
||||
DurationSpan.create(time.toLong()), 0, 1, 0
|
||||
@@ -182,14 +203,27 @@ class RoutePreviewScreen(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createRow(index: Int, action: Action): Row {
|
||||
val route = createRouteText(index)
|
||||
val titleText = "$index"
|
||||
return Row.Builder()
|
||||
.setTitle(route)
|
||||
.setOnClickListener(OnClickListener { onRouteSelected(index) })
|
||||
.addText(titleText)
|
||||
.addAction(action)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun onNavigate() {
|
||||
setResult(destination)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun onRouteSelected(index: Int) {
|
||||
setResult(destination)
|
||||
finish()
|
||||
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
|
||||
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||
//setResult(destination)
|
||||
//finish()
|
||||
}
|
||||
|
||||
fun getMapActionStrip(): ActionStrip {
|
||||
|
||||
@@ -10,18 +10,25 @@ 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.VALHALLA.ordinal
|
||||
private var routingEngine = RouteEngine.OSRM.ordinal
|
||||
|
||||
|
||||
init {
|
||||
routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
|
||||
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -38,6 +45,11 @@ class RoutingSettings(private val carContext: CarContext, private var viewModel:
|
||||
R.string.osrm,
|
||||
)
|
||||
)
|
||||
.addItem(
|
||||
buildRowForTemplate(
|
||||
R.string.tomtom,
|
||||
)
|
||||
)
|
||||
.setOnSelectedListener { index: Int ->
|
||||
this.onSelected(index)
|
||||
}
|
||||
@@ -58,7 +70,6 @@ class RoutingSettings(private val carContext: CarContext, private var viewModel:
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
private fun onSelected(index: Int) {
|
||||
setIntKeyValue(carContext, index, ROUTING_ENGINE)
|
||||
viewModel.routingEngine.value = index
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.kouros.navigation.car.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
@@ -17,9 +16,9 @@ import androidx.lifecycle.Observer
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.ViewStyle
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.data.Category
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.nominatim.SearchResult
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
@@ -28,15 +27,14 @@ import com.kouros.navigation.model.ViewModel
|
||||
class SearchScreen(
|
||||
carContext: CarContext,
|
||||
private var surfaceRenderer: SurfaceRenderer,
|
||||
private var location: Location,
|
||||
private val viewModel: ViewModel
|
||||
private val viewModel: ViewModel,
|
||||
) : Screen(carContext) {
|
||||
|
||||
var isSearchComplete: Boolean = false
|
||||
|
||||
var categories: List<Category> = listOf(
|
||||
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
|
||||
Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
|
||||
// Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
|
||||
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)),
|
||||
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites))
|
||||
)
|
||||
@@ -73,7 +71,6 @@ class SearchScreen(
|
||||
CategoriesScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
location,
|
||||
viewModel
|
||||
)
|
||||
) { obj: Any? ->
|
||||
@@ -89,7 +86,6 @@ class SearchScreen(
|
||||
PlaceListScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
location,
|
||||
it.id,
|
||||
viewModel
|
||||
)
|
||||
@@ -119,7 +115,7 @@ class SearchScreen(
|
||||
object : SearchCallback {
|
||||
override fun onSearchSubmitted(searchTerm: String) {
|
||||
isSearchComplete = true
|
||||
viewModel.searchPlaces(searchTerm, location)
|
||||
viewModel.searchPlaces(searchTerm, surfaceRenderer.lastLocation)
|
||||
}
|
||||
})
|
||||
.setHeaderAction(Action.BACK)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
package com.kouros.navigation.car
|
||||
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import com.kouros.navigation.data.Constants.home2Location
|
||||
import com.kouros.navigation.data.Constants.homeLocation
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
import com.kouros.navigation.data.SearchFilter
|
||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
@@ -24,16 +18,16 @@ class ViewModelTest {
|
||||
@Test
|
||||
fun routeViewModelTest() {
|
||||
|
||||
val fromLocation = Location(LocationManager.GPS_PROVIDER)
|
||||
fromLocation.isMock = true
|
||||
fromLocation.latitude = homeLocation.latitude
|
||||
fromLocation.longitude = homeLocation.longitude
|
||||
val toLocation = Location(LocationManager.GPS_PROVIDER)
|
||||
toLocation.isMock = true
|
||||
toLocation.latitude = home2Location.latitude
|
||||
toLocation.longitude = home2Location.longitude
|
||||
|
||||
val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
|
||||
// val fromLocation = Location(LocationManager.GPS_PROVIDER)
|
||||
// fromLocation.isMock = true
|
||||
// fromLocation.latitude = homeLocation.latitude
|
||||
// fromLocation.longitude = homeLocation.longitude
|
||||
// val toLocation = Location(LocationManager.GPS_PROVIDER)
|
||||
// toLocation.isMock = true
|
||||
// toLocation.latitude = home2Location.latitude
|
||||
// toLocation.longitude = home2Location.longitude
|
||||
//
|
||||
// val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
|
||||
//model.startNavigation(route)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ dependencies {
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.maplibre.compose)
|
||||
|
||||
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.net.Uri
|
||||
import com.kouros.navigation.data.route.Lane
|
||||
import com.kouros.navigation.utils.location
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -58,7 +59,7 @@ data class StepData (
|
||||
|
||||
var leftStepDistance: Double,
|
||||
|
||||
var maneuverType: Int,
|
||||
var currentManeuverType: Int,
|
||||
|
||||
var icon: Int,
|
||||
|
||||
@@ -66,7 +67,8 @@ data class StepData (
|
||||
|
||||
var leftDistance: Double,
|
||||
|
||||
var lane: List<Lane> = listOf(Lane(valid = false, indications = emptyList())),
|
||||
var lane: List<Lane> = listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
|
||||
var exitNumber: Int = 0,
|
||||
)
|
||||
|
||||
|
||||
@@ -75,39 +77,15 @@ data class Locations (
|
||||
var lat : Double,
|
||||
var lon : Double,
|
||||
var street : String = "",
|
||||
val search_filter: SearchFilter,
|
||||
val search_filter: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchFilter(
|
||||
var max_road_class: String = "",
|
||||
var exclude_toll : Boolean = false
|
||||
) {
|
||||
var avoidMotorway: Boolean = false,
|
||||
var avoidTollway : Boolean = false,
|
||||
|
||||
class Builder {
|
||||
private var avoidMotorway = false
|
||||
private var avoidTollway = false
|
||||
)
|
||||
|
||||
fun avoidMotorway (value: Boolean ) = apply {
|
||||
avoidMotorway = value
|
||||
}
|
||||
|
||||
fun avoidTollway (value: Boolean ) = apply {
|
||||
avoidTollway = value
|
||||
}
|
||||
|
||||
fun build(): SearchFilter {
|
||||
val filter = SearchFilter()
|
||||
if (avoidMotorway) {
|
||||
filter.max_road_class = "trunk"
|
||||
}
|
||||
if (avoidTollway) {
|
||||
filter.exclude_toll = true
|
||||
}
|
||||
return filter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ValhallaLocation (
|
||||
@@ -118,13 +96,6 @@ data class ValhallaLocation (
|
||||
var language: String
|
||||
)
|
||||
|
||||
data class BoundingBox (
|
||||
var southernLat : Double,
|
||||
var westernLon: Double,
|
||||
var northerLat : Double,
|
||||
var easternLon : Double
|
||||
)
|
||||
|
||||
object Constants {
|
||||
|
||||
//const val STYLE: String = "https://kouros-online.de/liberty.json"
|
||||
@@ -152,17 +123,8 @@ object Constants {
|
||||
|
||||
val categories = listOf("Tankstelle", "Apotheke", "Ladestationen")
|
||||
/** The initial location to use as an anchor for searches. */
|
||||
val homeLocation: Location = Location(LocationManager.GPS_PROVIDER)
|
||||
val home2Location: Location = Location(LocationManager.GPS_PROVIDER)
|
||||
|
||||
init {
|
||||
// Vogelhartstr. 17
|
||||
homeLocation.latitude = 48.185749
|
||||
homeLocation.longitude = 11.5793748
|
||||
// Hohenwaldeckstr. 27
|
||||
home2Location.latitude = 48.1164817
|
||||
home2Location.longitude = 11.594322
|
||||
}
|
||||
val homeVogelhart = location(11.5793748, 48.185749)
|
||||
val homeHohenwaldeck = location( 11.594322, 48.1164817)
|
||||
|
||||
const val SHARED_PREF_KEY = "NavigationPrefs"
|
||||
|
||||
@@ -174,19 +136,24 @@ object Constants {
|
||||
|
||||
const val AVOID_TOLLWAY = "AvoidTollway"
|
||||
|
||||
const val CAR_LOCATION = "CarLocation"
|
||||
const val ROUTING_ENGINE = "RoutingEngine"
|
||||
|
||||
const val NEXT_STEP_THRESHOLD = 100.0
|
||||
const val NEXT_STEP_THRESHOLD = 120.0
|
||||
|
||||
const val MAXIMAL_SNAP_CORRECTION = 50.0
|
||||
|
||||
const val MAXIMAL_ROUTE_DEVIATION = 100.0
|
||||
const val MAXIMAL_ROUTE_DEVIATION = 80.0
|
||||
|
||||
const val DESTINATION_ARRIVAL_DISTANCE = 40.0
|
||||
|
||||
const val NEAREST_LOCATION_DISTANCE = 10F
|
||||
|
||||
const val MAXIMUM_LOCATION_DISTANCE = 100000F
|
||||
|
||||
}
|
||||
|
||||
|
||||
enum class RouteEngine {
|
||||
VALHALLA, OSRM, GRAPHHOPPER
|
||||
VALHALLA, OSRM, TOMTOM, GRAPHHOPPER
|
||||
}
|
||||
|
||||
@@ -18,45 +18,63 @@ package com.kouros.navigation.data
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import org.json.JSONArray
|
||||
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
|
||||
import java.net.Authenticator
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.PasswordAuthentication
|
||||
import java.net.URL
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
|
||||
abstract class NavigationRepository {
|
||||
|
||||
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
|
||||
|
||||
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
|
||||
|
||||
abstract fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String
|
||||
|
||||
fun getRouteDistance(currentLocation: Location, location: Location, searchFilter: SearchFilter, context: Context): Double {
|
||||
val route = getRoute(currentLocation, location, searchFilter)
|
||||
val routeModel = RouteModel()
|
||||
routeModel.startNavigation(route, context)
|
||||
return routeModel.route.summary!!.distance
|
||||
abstract fun getRoute(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
carOrientation: Float,
|
||||
searchFilter: SearchFilter
|
||||
): String
|
||||
|
||||
abstract fun getTraffic(context: Context, location: Location, carOrientation: Float): String
|
||||
fun getRouteDistance(
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
carOrientation: Float,
|
||||
searchFilter: SearchFilter,
|
||||
context: Context
|
||||
): Double {
|
||||
val route = getRoute(context, currentLocation, location, carOrientation, searchFilter)
|
||||
val routeModel = RouteModel()
|
||||
routeModel.startNavigation(route, context)
|
||||
return routeModel.curRoute.summary.distance
|
||||
}
|
||||
|
||||
fun searchPlaces(search: String, location: Location) : String {
|
||||
// val bbox = getBoundingBox(location.longitude, location.latitude, 10.0)
|
||||
// val neLon = bbox["ne"]?.get("lon")
|
||||
// val neLat = bbox["ne"]?.get("lat")
|
||||
// val swLon = bbox["sw"]?.get("lon")
|
||||
// val swLat = bbox["sw"]?.get("lat")
|
||||
// val viewbox = "&viewbox=$swLon,$swLat,$neLon,$neLat"
|
||||
return fetchUrl("${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true,&countrycodes=de", false)
|
||||
fun searchPlaces(search: String, location: Location): String {
|
||||
val box = calculateSquareRadius(location.latitude, location.longitude, 100.0)
|
||||
val viewbox = "&bounded=1&viewbox=${box}"
|
||||
return fetchUrl(
|
||||
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
fun reverseAddress(location: Location) : String {
|
||||
return fetchUrl("${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true&countrycodes=de", false)
|
||||
fun reverseAddress(location: Location): String {
|
||||
return fetchUrl(
|
||||
"${nominatimUrl}reverse?lat=${location.latitude}&lon=${location.longitude}&format=jsonv2&addressdetails=true",
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
fun fetchUrl(url: String, authenticator : Boolean): String {
|
||||
|
||||
|
||||
fun fetchUrl(url: String, authenticator: Boolean): String {
|
||||
try {
|
||||
if (authenticator) {
|
||||
Authenticator.setDefault(object : Authenticator() {
|
||||
@@ -79,7 +97,7 @@ abstract class NavigationRepository {
|
||||
val responseCode = httpURLConnection.responseCode
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val response = httpURLConnection.inputStream.bufferedReader()
|
||||
.use { it.readText() } // defaults to UTF-8
|
||||
.use { it.readText() }
|
||||
return response
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package com.kouros.navigation.data
|
||||
|
||||
import android.location.Location
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.kouros.navigation.data.osrm.OsrmResponse
|
||||
import com.kouros.navigation.data.osrm.OsrmRoute
|
||||
import com.kouros.navigation.data.route.Leg
|
||||
import com.kouros.navigation.data.route.Maneuver
|
||||
import com.kouros.navigation.data.route.Step
|
||||
import com.kouros.navigation.data.route.Summary
|
||||
import com.kouros.navigation.data.tomtom.TomTomResponse
|
||||
import com.kouros.navigation.data.tomtom.TomTomRoute
|
||||
import com.kouros.navigation.data.valhalla.ValhallaResponse
|
||||
import com.kouros.navigation.data.valhalla.ValhallaRoute
|
||||
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
@@ -20,34 +21,23 @@ import org.maplibre.geojson.Point
|
||||
data class Route(
|
||||
|
||||
val routeEngine: Int,
|
||||
val summary: Summary?,
|
||||
val legs: List<Leg>?,
|
||||
val routeGeoJson: String = "",
|
||||
val centerLocation: Location = location(0.0, 0.0),
|
||||
var currentStep: Int = 0,
|
||||
val waypoints: List<List<Double>>?,
|
||||
val routes: List<com.kouros.navigation.data.route.Routes>,
|
||||
var currentStepIndex: Int = 0,
|
||||
) {
|
||||
|
||||
data class Builder(
|
||||
|
||||
var routeEngine: Int = 0,
|
||||
var summary: Summary? = null,
|
||||
var legs: List<Leg>? = null,
|
||||
var routeGeoJson: String = "",
|
||||
var centerLocation: Location = location(0.0, 0.0),
|
||||
var waypoints: List<List<Double>>? = null,
|
||||
var summary: Summary = Summary(),
|
||||
var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(),
|
||||
) {
|
||||
|
||||
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
|
||||
fun summary(summary: Summary) = apply { this.summary = summary }
|
||||
fun legs(legs: List<Leg>) = apply { this.legs = legs }
|
||||
fun routeGeoJson(routeGeoJson: String) = apply {
|
||||
this.routeGeoJson = routeGeoJson
|
||||
centerLocation = createCenterLocation(routeGeoJson)
|
||||
}
|
||||
fun routes(routes: List<com.kouros.navigation.data.route.Routes>) = apply {
|
||||
this.routes = routes
|
||||
|
||||
}
|
||||
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
|
||||
fun waypoints(waypoints: List<List<Double>>) = apply { this.waypoints = waypoints }
|
||||
|
||||
fun route(route: String) = apply {
|
||||
if (route.isNotEmpty() && route != "[]") {
|
||||
val gson = GsonBuilder().serializeNulls().create()
|
||||
@@ -60,12 +50,15 @@ data class Route(
|
||||
jsonObject["trip"].toString(),
|
||||
ValhallaResponse::class.java
|
||||
)
|
||||
ValhallaRoute().mapJsonToValhalla(routeJson, this)
|
||||
ValhallaRoute().mapToRoute(routeJson, this)
|
||||
}
|
||||
|
||||
else -> {
|
||||
RouteEngine.OSRM.ordinal -> {
|
||||
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
|
||||
OsrmRoute().mapToOsrm(osrmJson, this)
|
||||
OsrmRoute().mapToRoute(osrmJson, this)
|
||||
}
|
||||
else -> {
|
||||
val tomtomJson = gson.fromJson(route, TomTomResponse::class.java)
|
||||
TomTomRoute().mapToRoute(tomtomJson, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,17 +67,49 @@ data class Route(
|
||||
fun build(): Route {
|
||||
return Route(
|
||||
routeEngine = this.routeEngine,
|
||||
summary = this.summary,
|
||||
legs = this.legs,
|
||||
waypoints = this.waypoints,
|
||||
routeGeoJson = this.routeGeoJson,
|
||||
routes = this.routes,
|
||||
)
|
||||
}
|
||||
|
||||
fun buildEmpty(): Route {
|
||||
return Route(
|
||||
routeEngine = 0,
|
||||
routes = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun legs(): List<Leg> {
|
||||
return if (routes.isNotEmpty()) {
|
||||
routes.first().legs
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun isRouteValid(): Boolean {
|
||||
return routes.isNotEmpty() && legs().isNotEmpty()
|
||||
}
|
||||
fun currentStep(): Step {
|
||||
return if (isRouteValid()) {
|
||||
legs().first().steps[currentStepIndex]
|
||||
} else {
|
||||
Step(maneuver = Maneuver(waypoints = emptyList(), location = location(0.0, 0.0)))
|
||||
}
|
||||
}
|
||||
|
||||
fun nextStep(steps : Int): Step {
|
||||
val nextIndex = currentStepIndex + steps
|
||||
return if (isRouteValid() && nextIndex < legs().first().steps.size) {
|
||||
legs().first().steps[nextIndex]
|
||||
} else {
|
||||
currentStep()
|
||||
}
|
||||
}
|
||||
|
||||
fun maneuverLocations(): List<Point> {
|
||||
val step = currentStep()
|
||||
val waypoints = step.maneuver.waypoints
|
||||
val waypoints = currentStep().maneuver.waypoints
|
||||
val points = mutableListOf<Point>()
|
||||
for (loc in waypoints) {
|
||||
val point = Point.fromLngLat(loc[0], loc[1])
|
||||
@@ -92,21 +117,4 @@ data class Route(
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
fun currentStep(): Step {
|
||||
if (legs != null) {
|
||||
return legs.first().steps[currentStep]
|
||||
} else {
|
||||
throw IndexOutOfBoundsException("No legs available.")
|
||||
}
|
||||
}
|
||||
|
||||
fun nextStep(): Step {
|
||||
val nextIndex = currentStep + 1
|
||||
return if (nextIndex < legs!!.first().steps.size) {
|
||||
legs.first().steps[currentStep + 1]
|
||||
} else {
|
||||
throw IndexOutOfBoundsException("No next maneuver available.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Intersections(
|
||||
|
||||
@SerializedName("in") var inV: Int? = null,
|
||||
@SerializedName("out") var out: Int? = null,
|
||||
@SerializedName("in") var inV: Int = 0,
|
||||
@SerializedName("out") var out: Int = 0,
|
||||
@SerializedName("entry") var entry: ArrayList<Boolean> = arrayListOf(),
|
||||
@SerializedName("bearings") var bearings: ArrayList<Int> = arrayListOf(),
|
||||
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
|
||||
|
||||
@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Legs (
|
||||
|
||||
@SerializedName("steps" ) var steps : ArrayList<Steps> = arrayListOf(),
|
||||
@SerializedName("weight" ) var weight : Double? = null,
|
||||
@SerializedName("summary" ) var summary : String? = null,
|
||||
@SerializedName("duration" ) var duration : Double? = null,
|
||||
@SerializedName("distance" ) var distance : Double? = null
|
||||
@SerializedName("steps" ) var steps : List<Steps> = listOf(),
|
||||
@SerializedName("weight" ) var weight : Double = 0.0,
|
||||
@SerializedName("summary" ) var summary : String = "",
|
||||
@SerializedName("duration" ) var duration : Double = 0.0,
|
||||
@SerializedName("distance" ) var distance : Double = 0.0
|
||||
|
||||
)
|
||||
@@ -3,12 +3,13 @@ package com.kouros.navigation.data.osrm
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
|
||||
data class Maneuver (
|
||||
data class Maneuver(
|
||||
|
||||
@SerializedName("bearing_after" ) var bearingAfter : Int? = null,
|
||||
@SerializedName("bearing_before" ) var bearingBefore : Int? = null,
|
||||
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
|
||||
@SerializedName("modifier" ) var modifier : String? = null,
|
||||
@SerializedName("type" ) var type : String? = null
|
||||
@SerializedName("bearing_after") var bearingAfter: Int = 0,
|
||||
@SerializedName("bearing_before") var bearingBefore: Int = 0,
|
||||
@SerializedName("location") var location: ArrayList<Double> = arrayListOf(),
|
||||
@SerializedName("modifier") var modifier: String = "",
|
||||
@SerializedName("type") var type: String = "",
|
||||
@SerializedName("exit") var exit: Int = 0,
|
||||
|
||||
)
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.kouros.navigation.data.osrm
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
import com.kouros.navigation.data.SearchFilter
|
||||
@@ -8,11 +9,29 @@ private const val routeUrl = "https://kouros-online.de/osrm/route/v1/driving/"
|
||||
|
||||
class OsrmRepository : NavigationRepository() {
|
||||
override fun getRoute(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
carOrientation: Float,
|
||||
searchFilter: SearchFilter
|
||||
): String {
|
||||
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true"
|
||||
return fetchUrl(routeUrl + routeLocation, true)
|
||||
|
||||
var exclude = ""
|
||||
if (searchFilter.avoidMotorway) {
|
||||
exclude = "&exclude=motorway"
|
||||
}
|
||||
if (searchFilter.avoidTollway) {
|
||||
exclude = "$exclude&exclude=toll"
|
||||
}
|
||||
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=0"
|
||||
return fetchUrl(routeUrl + routeLocation + exclude, true)
|
||||
}
|
||||
|
||||
override fun getTraffic(
|
||||
context: Context,
|
||||
location: Location,
|
||||
carOrientation: Float
|
||||
): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class OsrmResponse (
|
||||
|
||||
@SerializedName("code" ) var code : String? = null,
|
||||
@SerializedName("code" ) var code : String = "",
|
||||
@SerializedName("routes" ) var routes : ArrayList<Routes> = arrayListOf(),
|
||||
@SerializedName("waypoints" ) var waypoints : ArrayList<Waypoints> = arrayListOf()
|
||||
|
||||
|
||||
@@ -1,57 +1,88 @@
|
||||
package com.kouros.navigation.data.osrm
|
||||
|
||||
import com.kouros.navigation.data.Route
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.route.Intersection
|
||||
import com.kouros.navigation.data.route.Lane
|
||||
import com.kouros.navigation.data.route.Leg
|
||||
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
|
||||
import com.kouros.navigation.data.route.Step
|
||||
import com.kouros.navigation.data.route.Summary
|
||||
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
||||
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
||||
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
||||
import com.kouros.navigation.utils.location
|
||||
|
||||
class OsrmRoute {
|
||||
|
||||
fun mapToOsrm(routeJson: OsrmResponse, builder: Route.Builder) {
|
||||
val waypoints = mutableListOf<List<Double>>()
|
||||
val summary = Summary()
|
||||
summary.distance = routeJson.routes.first().distance!! / 1000
|
||||
summary.duration = routeJson.routes.first().duration!! / 1000
|
||||
val steps = mutableListOf<Step>()
|
||||
fun mapToRoute(routeJson: OsrmResponse, builder: Route.Builder) {
|
||||
|
||||
val routes = mutableListOf<com.kouros.navigation.data.route.Routes>()
|
||||
var stepIndex = 0
|
||||
routeJson.routes.first().legs.first().steps.forEach {
|
||||
val intersections = mutableListOf<Intersection>()
|
||||
if (it.maneuver != null) {
|
||||
val points = decodePolyline(it.geometry!!, 5)
|
||||
waypoints.addAll(points)
|
||||
val maneuver = RouteManeuver(
|
||||
bearingBefore = it.maneuver!!.bearingBefore ?: 0,
|
||||
bearingAfter = it.maneuver!!.bearingAfter ?: 0,
|
||||
type = convertType(it.maneuver!!),
|
||||
waypoints = points
|
||||
)
|
||||
it.intersections.forEach { it2 ->
|
||||
if (it2.location[0] != 0.0) {
|
||||
val lanes = mutableListOf<Lane>()
|
||||
it2.lanes.forEach { it3 ->
|
||||
val lane = Lane(it3.valid, it3.indications)
|
||||
lanes.add(lane)
|
||||
routeJson.routes.forEach { route ->
|
||||
val legs = mutableListOf<Leg>()
|
||||
val waypoints = mutableListOf<List<Double>>()
|
||||
val summary = Summary(route.duration, route.distance / 1000)
|
||||
route.legs.forEach { leg ->
|
||||
val steps = mutableListOf<Step>()
|
||||
leg.steps.forEach { step ->
|
||||
val intersections = mutableListOf<Intersection>()
|
||||
val points = decodePolyline(step.geometry, 5)
|
||||
waypoints.addAll(points)
|
||||
val maneuver = RouteManeuver(
|
||||
bearingBefore = step.maneuver.bearingBefore,
|
||||
bearingAfter = step.maneuver.bearingAfter,
|
||||
type = convertType(step.maneuver),
|
||||
waypoints = points,
|
||||
exit = step.maneuver.exit,
|
||||
location = location(
|
||||
step.maneuver.location[0],
|
||||
step.maneuver.location[1]
|
||||
)
|
||||
)
|
||||
step.intersections.forEach { it2 ->
|
||||
if (it2.location[0] != 0.0) {
|
||||
val lanes = mutableListOf<Lane>()
|
||||
it2.lanes.forEach { it3 ->
|
||||
if (it3.indications.isNotEmpty() && it3.indications.first() != "none") {
|
||||
val lane = Lane(
|
||||
location(it2.location[0], it2.location[1]),
|
||||
it3.valid,
|
||||
it3.indications
|
||||
)
|
||||
lanes.add(lane)
|
||||
}
|
||||
}
|
||||
intersections.add(Intersection(it2.location, lanes))
|
||||
}
|
||||
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
|
||||
}
|
||||
val step = Step( index = stepIndex, name = it.name!!, distance = it.distance!! / 1000, duration = it.duration!!, maneuver = maneuver, intersection = intersections)
|
||||
steps.add(step)
|
||||
stepIndex += 1
|
||||
legs.add(Leg(steps))
|
||||
}
|
||||
val routeGeoJson = createLineStringCollection(waypoints)
|
||||
val centerLocation = createCenterLocation(createLineStringCollection(waypoints))
|
||||
val newRoute = com.kouros.navigation.data.route.Routes(
|
||||
legs,
|
||||
summary,
|
||||
routeGeoJson,
|
||||
centerLocation = centerLocation,
|
||||
waypoints = waypoints
|
||||
)
|
||||
routes.add(newRoute)
|
||||
}
|
||||
val leg = Leg(steps)
|
||||
builder
|
||||
.routeType(1)
|
||||
.summary(summary)
|
||||
.routeGeoJson(createLineStringCollection(waypoints))
|
||||
.legs(listOf(leg))
|
||||
.waypoints(waypoints.toList())
|
||||
.routeType(RouteEngine.OSRM.ordinal)
|
||||
.routes(routes)
|
||||
}
|
||||
|
||||
fun convertType(maneuver: Maneuver): Int {
|
||||
@@ -60,60 +91,138 @@ class OsrmRoute {
|
||||
ManeuverType.depart.value -> {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DEPART
|
||||
}
|
||||
|
||||
ManeuverType.arrive.value -> {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION
|
||||
if (maneuver.modifier == "right") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_RIGHT
|
||||
}
|
||||
if (maneuver.modifier == "left") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_LEFT
|
||||
}
|
||||
if (maneuver.modifier == "straight") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_DESTINATION_STRAIGHT
|
||||
}
|
||||
}
|
||||
|
||||
ManeuverType.continue_.value -> {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
||||
if (maneuver.modifier == "right") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
|
||||
}
|
||||
if (maneuver.modifier == "left") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
|
||||
}
|
||||
}
|
||||
|
||||
ManeuverType.newName.value -> {
|
||||
if (maneuver.modifier == "straight") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
||||
}
|
||||
if (maneuver.modifier == "slight right") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||
}
|
||||
if (maneuver.modifier == "slight left") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
||||
}
|
||||
}
|
||||
|
||||
ManeuverType.turn.value,
|
||||
ManeuverType.endOfRoad.value -> {
|
||||
if (maneuver.modifier == "right") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_RIGHT
|
||||
}
|
||||
if (maneuver.modifier == "left") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_NORMAL_LEFT
|
||||
}
|
||||
if (maneuver.modifier == "straight") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_STRAIGHT
|
||||
}
|
||||
}
|
||||
ManeuverType.turn.value,
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
ManeuverType.fork.value
|
||||
-> {
|
||||
if (maneuver.modifier == "slight right") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||
}
|
||||
}
|
||||
|
||||
ManeuverType.merge.value
|
||||
-> {
|
||||
if (maneuver.modifier == "slight left") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_LEFT
|
||||
}
|
||||
if (maneuver.modifier == "slight right") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SLIGHT_RIGHT
|
||||
}
|
||||
}
|
||||
ManeuverType.roundAbout.value
|
||||
-> {
|
||||
if (maneuver.modifier == "right") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW
|
||||
}
|
||||
}
|
||||
ManeuverType.exitRoundabout.value
|
||||
-> {
|
||||
if (maneuver.modifier == "right") {
|
||||
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_EXIT_CCW
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newType
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
enum class ManeuverType(val value: String) {
|
||||
turn("turn"),
|
||||
depart("depart"),
|
||||
arrive("arrive"),
|
||||
merge("merge"),
|
||||
onRamp("on ramp"),
|
||||
offRamp("off ramp"),
|
||||
fork("fork"),
|
||||
endOfRoad("end of road"),
|
||||
continue_("continue"),
|
||||
roundAbout("roundabout"),
|
||||
rotary("rotary"),
|
||||
roundaboutTurn("roundabout turn"),
|
||||
notification("notification"),
|
||||
exitRoundabout("exit roundabout"),
|
||||
exitRotary("exit rotary")
|
||||
|
||||
enum class ManeuverType(val value: String) {
|
||||
turn("turn"),
|
||||
depart("depart"),
|
||||
arrive("arrive"),
|
||||
merge("merge"),
|
||||
onRamp("on ramp"),
|
||||
offRamp("off ramp"),
|
||||
fork("fork"),
|
||||
endOfRoad("end of road"),
|
||||
continue_("continue"),
|
||||
roundAbout("roundabout"),
|
||||
rotary("rotary"),
|
||||
roundaboutTurn("roundabout turn"),
|
||||
notification("notification"),
|
||||
exitRoundabout("exit roundabout"),
|
||||
exitRotary("exit rotary"),
|
||||
newName("new name"),
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ data class Routes (
|
||||
@SerializedName("legs" ) var legs : ArrayList<Legs> = arrayListOf(),
|
||||
@SerializedName("weight_name" ) var weightName : String? = null,
|
||||
@SerializedName("geometry" ) var geometry : String? = null,
|
||||
@SerializedName("weight" ) var weight : Double? = null,
|
||||
@SerializedName("duration" ) var duration : Double? = null,
|
||||
@SerializedName("distance" ) var distance : Double? = null
|
||||
@SerializedName("weight" ) var weight : Double = 0.0,
|
||||
@SerializedName("duration" ) var duration : Double = 0.0,
|
||||
@SerializedName("distance" ) var distance : Double = 0.0
|
||||
|
||||
)
|
||||
@@ -6,13 +6,13 @@ import com.google.gson.annotations.SerializedName
|
||||
data class Steps (
|
||||
|
||||
@SerializedName("intersections" ) var intersections : ArrayList<Intersections> = arrayListOf(),
|
||||
@SerializedName("driving_side" ) var drivingSide : String? = null,
|
||||
@SerializedName("geometry" ) var geometry : String? = null,
|
||||
@SerializedName("maneuver" ) var maneuver : Maneuver? = Maneuver(),
|
||||
@SerializedName("name" ) var name : String? = null,
|
||||
@SerializedName("mode" ) var mode : String? = null,
|
||||
@SerializedName("weight" ) var weight : Double? = null,
|
||||
@SerializedName("duration" ) var duration : Double? = null,
|
||||
@SerializedName("distance" ) var distance : Double? = null
|
||||
@SerializedName("driving_side" ) var drivingSide : String = "",
|
||||
@SerializedName("geometry" ) var geometry : String = "",
|
||||
@SerializedName("maneuver" ) val maneuver : Maneuver = Maneuver(),
|
||||
@SerializedName("name" ) var name : String = "",
|
||||
@SerializedName("mode" ) var mode : String = "",
|
||||
@SerializedName("weight" ) var weight : Double = 0.0,
|
||||
@SerializedName("duration" ) var duration : Double = 0.0,
|
||||
@SerializedName("distance" ) var distance : Double = 0.0,
|
||||
|
||||
)
|
||||
@@ -5,9 +5,9 @@ import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Waypoints (
|
||||
|
||||
@SerializedName("hint" ) var hint : String? = null,
|
||||
@SerializedName("hint" ) var hint : String = "",
|
||||
@SerializedName("location" ) var location : ArrayList<Double> = arrayListOf(),
|
||||
@SerializedName("name" ) var name : String? = null,
|
||||
@SerializedName("distance" ) var distance : Double? = null
|
||||
@SerializedName("name" ) var name : String = "",
|
||||
@SerializedName("distance" ) var distance : Double = 0.0,
|
||||
|
||||
)
|
||||
@@ -5,10 +5,10 @@ import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Elements (
|
||||
|
||||
@SerializedName("type" ) var type : String? = null,
|
||||
@SerializedName("id" ) var id : Long? = null,
|
||||
@SerializedName("lat" ) var lat : Double? = null,
|
||||
@SerializedName("lon" ) var lon : Double? = null,
|
||||
@SerializedName("type" ) var type : String = "",
|
||||
@SerializedName("id" ) var id : Long = 0,
|
||||
@SerializedName("lat" ) var lat : Double = 0.0,
|
||||
@SerializedName("lon" ) var lon : Double = 0.0,
|
||||
@SerializedName("tags" ) var tags : Tags = Tags(),
|
||||
var distance : Double = 0.0
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ package com.kouros.navigation.data.overpass
|
||||
|
||||
import android.location.Location
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.kouros.navigation.utils.GeoUtils.getOverpassBbox
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.kouros.navigation.utils.GeoUtils.getBoundingBox
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
@@ -14,7 +13,7 @@ class Overpass {
|
||||
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
|
||||
|
||||
|
||||
fun getAround(radius: Int, linestring: String) : List<Elements> {
|
||||
fun getAround(radius: Int, linestring: String): List<Elements> {
|
||||
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
||||
httpURLConnection.requestMethod = "POST"
|
||||
httpURLConnection.setRequestProperty(
|
||||
@@ -41,7 +40,7 @@ class Overpass {
|
||||
location: Location,
|
||||
radius: Double
|
||||
): List<Elements> {
|
||||
val boundingBox = getOverpassBbox(location, radius)
|
||||
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
|
||||
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
|
||||
httpURLConnection.requestMethod = "POST"
|
||||
httpURLConnection.setRequestProperty(
|
||||
@@ -58,12 +57,13 @@ class Overpass {
|
||||
| node[$type=$category]
|
||||
| ($boundingBox);
|
||||
|);
|
||||
|(._;>;);
|
||||
|out body;
|
||||
""".trimMargin()
|
||||
return overpassApi(httpURLConnection, searchQuery)
|
||||
}
|
||||
|
||||
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String) : List<Elements> {
|
||||
fun overpassApi(httpURLConnection: HttpURLConnection, searchQuery: String): List<Elements> {
|
||||
try {
|
||||
val outputStreamWriter = OutputStreamWriter(httpURLConnection.outputStream)
|
||||
outputStreamWriter.write(searchQuery)
|
||||
@@ -76,8 +76,10 @@ class Overpass {
|
||||
val gson = GsonBuilder().serializeNulls().create()
|
||||
val overpass = gson.fromJson(response, Amenity::class.java)
|
||||
return overpass.elements
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ data class Tags(
|
||||
@SerializedName("ref") var ref: String? = null,
|
||||
@SerializedName("socket:type2") var socketType2: String? = null,
|
||||
@SerializedName("socket:type2:output") var socketType2Output: String? = null,
|
||||
@SerializedName("maxspeed") var maxspeed: String? = null,
|
||||
@SerializedName("maxspeed") var maxspeed: String = "0",
|
||||
@SerializedName("direction") var direction: String? = null,
|
||||
|
||||
)
|
||||
@@ -3,6 +3,6 @@ package com.kouros.navigation.data.route
|
||||
import java.util.Collections
|
||||
|
||||
data class Intersection(
|
||||
val location: ArrayList<Double> = arrayListOf(0.0, 0.0),
|
||||
val location: List<Double> = listOf(0.0, 0.0),
|
||||
val lane : List<Lane> = Collections.emptyList<Lane>(),
|
||||
)
|
||||
|
||||
@@ -1,6 +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>,
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.kouros.navigation.data.route
|
||||
|
||||
data class Leg(
|
||||
var steps : List<Step> = arrayListOf()
|
||||
var steps : List<Step> = arrayListOf(),
|
||||
var intersection: List<Intersection> = arrayListOf()
|
||||
)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.kouros.navigation.data.route
|
||||
|
||||
import android.location.Location
|
||||
|
||||
data class Maneuver(
|
||||
val bearingBefore : Int = 0,
|
||||
val bearingAfter : Int = 0,
|
||||
val type: Int = 0,
|
||||
val waypoints: List<List<Double>>,
|
||||
val location: Location,
|
||||
val exit: Int = 0,
|
||||
val street: String = "",
|
||||
)
|
||||
|
||||
@@ -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>>,
|
||||
)
|
||||
@@ -10,6 +10,6 @@ data class Step(
|
||||
val maneuver: Maneuver,
|
||||
val duration: Double = 0.0,
|
||||
val distance: Double = 0.0,
|
||||
val name : String = "",
|
||||
val street : String = "",
|
||||
val intersection: List<Intersection> = mutableListOf(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.kouros.navigation.data.route
|
||||
|
||||
data class Summary(
|
||||
// sec
|
||||
var duration : Double = 0.0,
|
||||
// km
|
||||
var distance : Double = 0.0,
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.kouros.navigation.data.tomtom
|
||||
|
||||
data class Cause(
|
||||
val mainCauseCode: Int
|
||||
)
|
||||
@@ -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
|
||||
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.kouros.navigation.data.tomtom
|
||||
|
||||
data class Guidance(
|
||||
val instructionGroups: List<InstructionGroup>,
|
||||
val instructions: List<Instruction>
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
)
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.kouros.navigation.data.tomtom
|
||||
|
||||
data class Lane(
|
||||
val directions: List<String>,
|
||||
val follow: String
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.kouros.navigation.data.tomtom
|
||||
|
||||
data class Leg(
|
||||
val encodedPolyline: String,
|
||||
val encodedPolylinePrecision: Int,
|
||||
val summary: SummaryX
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.kouros.navigation.data.tomtom
|
||||
|
||||
data class Point(
|
||||
val latitude: Double,
|
||||
val longitude: Double
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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§ionType=traffic&report=effectiveSettings&routeType=eco" +
|
||||
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
|
||||
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
|
||||
"&instructionsType=text&language=en-GB§ionType=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\":"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.kouros.navigation.data.tomtom
|
||||
|
||||
data class TomTomResponse(
|
||||
val formatVersion: String,
|
||||
val routes: List<Route>
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.kouros.navigation.data.tomtom
|
||||
|
||||
data class TrafficData (
|
||||
var traffic : Traffic ,
|
||||
var trafficData: String = ""
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.kouros.navigation.data.valhalla
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import com.kouros.navigation.data.Locations
|
||||
import com.kouros.navigation.data.NavigationRepository
|
||||
@@ -12,11 +13,29 @@ private const val routeUrl = "https://kouros-online.de/valhalla/route?json="
|
||||
|
||||
class ValhallaRepository : NavigationRepository() {
|
||||
|
||||
override fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
|
||||
SearchFilter
|
||||
override fun getRoute(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
carOrientation: Float,
|
||||
searchFilter: SearchFilter
|
||||
): String {
|
||||
|
||||
var exclude = ""
|
||||
if (searchFilter.avoidMotorway) {
|
||||
exclude = "&max_road_class='trunk'"
|
||||
}
|
||||
if (searchFilter.avoidTollway) {
|
||||
exclude = "$exclude&exclude_toll=true"
|
||||
}
|
||||
|
||||
val vLocation = listOf(
|
||||
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
|
||||
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
|
||||
Locations(
|
||||
lat = currentLocation.latitude,
|
||||
lon = currentLocation.longitude,
|
||||
search_filter = exclude
|
||||
),
|
||||
Locations(lat = location.latitude, lon = location.longitude, search_filter = exclude)
|
||||
)
|
||||
val valhallaLocation = ValhallaLocation(
|
||||
locations = vLocation,
|
||||
@@ -28,4 +47,12 @@ class ValhallaRepository : NavigationRepository() {
|
||||
val routeLocation = Json.encodeToString(valhallaLocation)
|
||||
return fetchUrl(routeUrl + routeLocation, true)
|
||||
}
|
||||
|
||||
override fun getTraffic(
|
||||
context: Context,
|
||||
location: Location,
|
||||
carOrientation: Float
|
||||
): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,20 @@ package com.kouros.navigation.data.valhalla
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.data.Route
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.route.Leg
|
||||
import com.kouros.navigation.data.route.Maneuver as RouteManeuver
|
||||
import com.kouros.navigation.data.route.Step
|
||||
import com.kouros.navigation.data.route.Summary
|
||||
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
|
||||
import com.kouros.navigation.utils.GeoUtils.decodePolyline
|
||||
import com.kouros.navigation.utils.location
|
||||
|
||||
class ValhallaRoute {
|
||||
|
||||
fun mapJsonToValhalla(routeJson: ValhallaResponse, builder: Route.Builder) {
|
||||
fun mapToRoute(routeJson: ValhallaResponse, builder: Route.Builder) {
|
||||
val waypoints = decodePolyline(routeJson.legs[0].shape)
|
||||
val summary = Summary()
|
||||
summary.distance = routeJson.summaryValhalla.length
|
||||
summary.duration = routeJson.summaryValhalla.time
|
||||
val summary = Summary(routeJson.summaryValhalla.time, routeJson.summaryValhalla.length)
|
||||
val steps = mutableListOf<Step>()
|
||||
var stepIndex = 0
|
||||
routeJson.legs[0].maneuvers.forEach {
|
||||
@@ -25,23 +25,23 @@ class ValhallaRoute {
|
||||
bearingAfter = it.bearingAfter,
|
||||
//type = it.type,
|
||||
type = convertType(it),
|
||||
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1)
|
||||
waypoints =waypoints.subList(it.beginShapeIndex, it.endShapeIndex+1),
|
||||
// TODO: calculate from ShapeIndex !
|
||||
location = location(0.0, 0.0)
|
||||
|
||||
)
|
||||
var name = ""
|
||||
if (it.streetNames != null && it.streetNames.isNotEmpty()) {
|
||||
name = it.streetNames[0]
|
||||
}
|
||||
val step = Step( index = stepIndex, name = name, distance = it.length, duration = it.time, maneuver = maneuver)
|
||||
val step = Step( index = stepIndex, street = name, distance = it.length, duration = it.time, maneuver = maneuver)
|
||||
steps.add(step)
|
||||
stepIndex += 1
|
||||
}
|
||||
val leg = Leg(steps)
|
||||
builder
|
||||
.routeType(1)
|
||||
.summary(summary)
|
||||
.routeGeoJson(createLineStringCollection(waypoints))
|
||||
.legs(listOf(leg))
|
||||
.waypoints(waypoints)
|
||||
.routeType(RouteEngine.VALHALLA.ordinal)
|
||||
// TODO
|
||||
.routes(emptyList())
|
||||
}
|
||||
|
||||
fun convertType(maneuver: Maneuvers): Int {
|
||||
|
||||
@@ -14,7 +14,6 @@ class BaseStyleModel {
|
||||
}
|
||||
|
||||
fun readStyle(context: Context, darkModeSettings: Int, isCarDarkMode: Boolean): BaseStyle.Json {
|
||||
println("BaseStyle ${isDarkTheme(context)}")
|
||||
val liberty = when(darkModeSettings) {
|
||||
0 -> context.resources.openRawResource(R.raw.liberty)
|
||||
1 -> context.resources.openRawResource(R.raw.liberty_night)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,407 +1,157 @@
|
||||
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 android.location.Location
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
import androidx.car.app.navigation.model.Step
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
|
||||
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.Route
|
||||
import com.kouros.navigation.data.StepData
|
||||
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.valhalla.ManeuverType
|
||||
import com.kouros.navigation.utils.GeoUtils.createCenterLocation
|
||||
import com.kouros.navigation.data.route.Routes
|
||||
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
open class RouteModel() {
|
||||
data class RouteState(
|
||||
val route: Route? = null,
|
||||
val isNavigating: Boolean = false,
|
||||
val destination: Place = Place(),
|
||||
open class RouteModel {
|
||||
|
||||
// Immutable Data Class
|
||||
data class NavigationState(
|
||||
val route: Route = Route.Builder().buildEmpty(),
|
||||
val iconMapper : IconMapper = IconMapper(),
|
||||
val navigating: Boolean = false,
|
||||
val arrived: Boolean = false,
|
||||
val maneuverType: Int = 0,
|
||||
val travelMessage: String = "",
|
||||
val lastSpeedLocation: Location = location(0.0, 0.0),
|
||||
val lastSpeedIndex: Int = 0,
|
||||
val maxSpeed: Int = 0,
|
||||
val location: Location = location(0.0, 0.0),
|
||||
val maneuverType: Int = 0,
|
||||
val lastLocation: Location = location(0.0, 0.0),
|
||||
val bearing : Float = 0F
|
||||
val currentLocation: Location = location(0.0, 0.0),
|
||||
val routeBearing: Float = 0F,
|
||||
val currentRouteIndex: Int = 0,
|
||||
val destination: Place = Place()
|
||||
)
|
||||
|
||||
var routeState = RouteState()
|
||||
var navState = NavigationState()
|
||||
|
||||
var route: Route
|
||||
get() = routeState.route!!
|
||||
set(value) {
|
||||
routeState = routeState.copy(route = value)
|
||||
}
|
||||
val route: Route
|
||||
get() = navState.route
|
||||
|
||||
val routeCalculator : RouteCalculator = RouteCalculator(this)
|
||||
|
||||
val legs: Leg
|
||||
get() = routeState.route!!.legs!!.first()
|
||||
val curRoute: Routes
|
||||
get() = navState.route.routes[navState.currentRouteIndex]
|
||||
|
||||
val curLeg: Leg
|
||||
get() = navState.route.routes[navState.currentRouteIndex].legs.first()
|
||||
|
||||
fun startNavigation(routeString: String, context: Context) {
|
||||
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
|
||||
var newRoute = Route.Builder()
|
||||
.routeEngine(routeEngine)
|
||||
.route(routeString)
|
||||
.build()
|
||||
// TODO:
|
||||
newRoute = newRoute.copy(centerLocation = createCenterLocation(newRoute.routeGeoJson))
|
||||
println("Route ${newRoute.centerLocation}")
|
||||
this.routeState = routeState.copy(
|
||||
route = newRoute,
|
||||
isNavigating = true
|
||||
navState = navState.copy(
|
||||
route = Route.Builder()
|
||||
.routeEngine(routeEngine)
|
||||
.route(routeString)
|
||||
.build()
|
||||
)
|
||||
if (hasLegs()) {
|
||||
navState = navState.copy(navigating = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasLegs(): Boolean {
|
||||
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty()
|
||||
}
|
||||
|
||||
fun stopNavigation() {
|
||||
this.routeState = routeState.copy(
|
||||
route = null,
|
||||
isNavigating = false,
|
||||
navState = navState.copy(
|
||||
route = Route.Builder().buildEmpty(),
|
||||
navigating = false,
|
||||
arrived = false,
|
||||
maneuverType = 0,
|
||||
maneuverType = Maneuver.TYPE_UNKNOWN
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun updateLocation(location: Location, viewModel: ViewModel) {
|
||||
routeState = routeState.copy(location = location)
|
||||
findStep(location)
|
||||
updateSpeedLimit(location, viewModel)
|
||||
|
||||
fun updateLocation(curLocation: Location, viewModel: ViewModel) {
|
||||
navState = navState.copy(currentLocation = curLocation)
|
||||
routeCalculator.findStep(curLocation)
|
||||
routeCalculator.updateSpeedLimit(curLocation, viewModel)
|
||||
navState = navState.copy(lastLocation = navState.currentLocation)
|
||||
}
|
||||
|
||||
private fun findStep(location: Location) {
|
||||
var nearestDistance = 100000.0f
|
||||
for ((index, step) in legs.steps.withIndex()) {
|
||||
if (index >= route.currentStep) {
|
||||
for ((wayIndex, waypoint) in step.maneuver.waypoints.withIndex()) {
|
||||
if (wayIndex >= step.waypointIndex) {
|
||||
val distance = location.distanceTo(location(waypoint[0], waypoint[1]))
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance
|
||||
route.currentStep = step.index
|
||||
step.waypointIndex = wayIndex
|
||||
step.wayPointLocation = location(waypoint[0], waypoint[1])
|
||||
val bearing = routeState.lastLocation.bearingTo(location)
|
||||
this.routeState = routeState.copy(lastLocation = location, bearing = bearing)
|
||||
}
|
||||
}
|
||||
if (nearestDistance == 0F) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nearestDistance == 0F) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentIntersection(location: Location): Intersection {
|
||||
var inter = Intersection()
|
||||
var nearestDistance = 100000.0f
|
||||
route.currentStep().intersection.forEach {
|
||||
val distance = location.distanceTo(location(it.location[0], it.location[1]))
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance
|
||||
inter = it
|
||||
}
|
||||
}
|
||||
return inter
|
||||
}
|
||||
|
||||
fun updateSpeedLimit(location: Location, viewModel: ViewModel) = runBlocking {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// speed limit
|
||||
val distance = routeState.lastSpeedLocation.distanceTo(location)
|
||||
if (distance > 500 || routeState.lastSpeedIndex < route.currentStep) {
|
||||
routeState = routeState.copy(lastSpeedIndex = route.currentStep)
|
||||
routeState = routeState.copy(lastSpeedLocation = location)
|
||||
val elements = viewModel.getMaxSpeed(location)
|
||||
elements.forEach {
|
||||
if (it.tags.name != null && it.tags.maxspeed != null) {
|
||||
val speed = it.tags.maxspeed!!.toInt()
|
||||
routeState = routeState.copy(maxSpeed = speed)
|
||||
private fun currentLanes(): List<Lane> {
|
||||
var lanes = emptyList<Lane>()
|
||||
if (navState.route.legs().isNotEmpty()) {
|
||||
navState.route.legs().first().intersection.forEach {
|
||||
if (it.lane.isNotEmpty()) {
|
||||
val distance =
|
||||
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
|
||||
val sectionBearing =
|
||||
navState.lastLocation.bearingTo(location(it.location[0], it.location[1]))
|
||||
if (distance < 500 && (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
|
||||
lanes = it.lane
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lanes
|
||||
}
|
||||
|
||||
fun currentStep(): StepData {
|
||||
val currentStep = route.currentStep()
|
||||
// Determine if we should display the current or the next maneuver
|
||||
val distanceToNextStep = leftStepDistance()
|
||||
val isNearNextManeuver = distanceToNextStep in 0.0..NEXT_STEP_THRESHOLD
|
||||
val shouldAdvance =
|
||||
isNearNextManeuver && route.currentStep < (route.legs!!.first().steps.size)
|
||||
|
||||
val distanceToNextStep = routeCalculator.leftStepDistance()
|
||||
// Determine the maneuver type and corresponding icon
|
||||
var maneuverType = if (hasArrived(currentStep.maneuver.type)) {
|
||||
currentStep.maneuver.type
|
||||
} else {
|
||||
Maneuver.TYPE_STRAIGHT
|
||||
}
|
||||
// Get the single, correct maneuver for this state
|
||||
val relevantStep = if (shouldAdvance) {
|
||||
route.nextStep() // This advances the route's state
|
||||
} else {
|
||||
route.currentStep()
|
||||
}
|
||||
val currentStep = navState.route.nextStep(0)
|
||||
// Safely get the street name from the maneuver
|
||||
val streetName = relevantStep.name
|
||||
if (shouldAdvance) {
|
||||
maneuverType = relevantStep.maneuver.type
|
||||
}
|
||||
val maneuverIconPair = maneuverIcon(maneuverType)
|
||||
routeState = routeState.copy(maneuverType = maneuverIconPair.first)
|
||||
val streetName = currentStep.maneuver.street
|
||||
val curManeuverType = currentStep.maneuver.type
|
||||
val exitNumber = currentStep.maneuver.exit
|
||||
val maneuverIcon = navState.iconMapper.maneuverIcon(curManeuverType)
|
||||
navState = navState.copy(maneuverType = curManeuverType)
|
||||
|
||||
val lanes = currentLanes()
|
||||
|
||||
// Construct and return the final StepData object
|
||||
val intersection = currentIntersection(routeState.location)
|
||||
return StepData(
|
||||
streetName,
|
||||
distanceToNextStep,
|
||||
maneuverIconPair.first,
|
||||
maneuverIconPair.second,
|
||||
arrivalTime(),
|
||||
travelLeftDistance(),
|
||||
intersection.lane
|
||||
navState.maneuverType,
|
||||
maneuverIcon,
|
||||
routeCalculator.arrivalTime(),
|
||||
routeCalculator.travelLeftDistance(),
|
||||
lanes,
|
||||
exitNumber
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun nextStep(): StepData {
|
||||
val step = route.nextStep()
|
||||
val step = navState.route.nextStep(1)
|
||||
val maneuverType = step.maneuver.type
|
||||
val distanceLeft = leftStepDistance()
|
||||
val distanceLeft = routeCalculator.leftStepDistance()
|
||||
var text = ""
|
||||
when (distanceLeft) {
|
||||
in 0.0..NEXT_STEP_THRESHOLD -> {
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (step.name.isNotEmpty()) {
|
||||
text = step.name
|
||||
if (step.street.isNotEmpty()) {
|
||||
text = step.street
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val routing: (Pair<Int, Int>) = maneuverIcon(maneuverType)
|
||||
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
|
||||
// Construct and return the final StepData object
|
||||
return StepData(
|
||||
text,
|
||||
distanceLeft,
|
||||
routing.first,
|
||||
routing.second,
|
||||
arrivalTime(),
|
||||
travelLeftDistance()
|
||||
maneuverType,
|
||||
maneuverIcon,
|
||||
routeCalculator.arrivalTime(),
|
||||
routeCalculator.travelLeftDistance(),
|
||||
listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
|
||||
step.maneuver.exit
|
||||
)
|
||||
}
|
||||
|
||||
fun travelLeftTime(): Double {
|
||||
var timeLeft = 0.0
|
||||
// time for next step until end step
|
||||
for (i in route.currentStep + 1..<legs.steps.size) {
|
||||
val step = legs.steps[i]
|
||||
timeLeft += step.duration
|
||||
}
|
||||
// time for current step
|
||||
val step = route.currentStep()
|
||||
val curTime = step.duration
|
||||
val percent =
|
||||
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
|
||||
val time = curTime * percent / 100
|
||||
timeLeft += time
|
||||
return timeLeft
|
||||
}
|
||||
|
||||
fun arrivalTime(): Long {
|
||||
val timeLeft = travelLeftTime()
|
||||
// Calculate the time to destination from the current time.
|
||||
val nowUtcMillis = System.currentTimeMillis()
|
||||
val timeToDestinationMillis =
|
||||
TimeUnit.SECONDS.toMillis(timeLeft.toLong())
|
||||
return nowUtcMillis + timeToDestinationMillis
|
||||
}
|
||||
|
||||
/** Returns the current [Step] left distance in m. */
|
||||
fun leftStepDistance(): Double {
|
||||
val step = route.currentStep()
|
||||
var leftDistance = step.distance
|
||||
val percent =
|
||||
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
|
||||
leftDistance = leftDistance * percent / 100
|
||||
// The remaining distance to the step, rounded to the nearest 10 units.
|
||||
return (leftDistance * 1000 / 10.0).roundToInt() * 10.0
|
||||
|
||||
}
|
||||
|
||||
/** Returns the left distance in km. */
|
||||
fun travelLeftDistance(): Double {
|
||||
var leftDistance = 0.0
|
||||
for (i in route.currentStep + 1..<legs.steps.size) {
|
||||
val step = route.legs!![0].steps[i]
|
||||
leftDistance += step.distance
|
||||
}
|
||||
|
||||
val step = route.currentStep()
|
||||
val curDistance = step.distance
|
||||
val percent =
|
||||
100 * (step.maneuver.waypoints.size - step.waypointIndex) / (step.maneuver.waypoints.size)
|
||||
val distance = curDistance * percent / 100
|
||||
leftDistance += distance
|
||||
return leftDistance
|
||||
}
|
||||
|
||||
fun maneuverIcon(routeManeuverType: Int): (Pair<Int, Int>) {
|
||||
var currentTurnIcon = R.drawable.ic_turn_name_change
|
||||
when (routeManeuverType) {
|
||||
Maneuver.TYPE_STRAIGHT -> {
|
||||
currentTurnIcon = R.drawable.ic_turn_name_change
|
||||
}
|
||||
|
||||
Maneuver.TYPE_DESTINATION,
|
||||
-> {
|
||||
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 Pair(routeManeuverType, currentTurnIcon)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fun createLaneIcon(context: Context, stepData: StepData): IconCompat {
|
||||
val bitmaps = mutableListOf<Bitmap>()
|
||||
stepData.lane.forEach {
|
||||
if (it.indications.isNotEmpty()) {
|
||||
it.indications.forEach { it2 ->
|
||||
val resource = laneToResource(it2, it, stepData)
|
||||
if (it2 != "none") {
|
||||
println("Direction $resource")
|
||||
if (resource.isNotEmpty()) {
|
||||
val id = resourceId( resource);
|
||||
val bitMap = BitmapFactory.decodeResource(context.resources, id)
|
||||
bitmaps.add(bitMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 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,
|
||||
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 * 40F, 0F)
|
||||
canvas.drawBitmap(it, matrix, null)
|
||||
}
|
||||
i++
|
||||
}
|
||||
return bmOverlay
|
||||
}
|
||||
|
||||
private fun laneToResource(direction: String, lane: Lane, stepData: StepData): String {
|
||||
println("Maneuver ${stepData.maneuverType}")
|
||||
return when (val direction = direction.replace(" ", "_")) {
|
||||
"left" -> if (stepData.maneuverType == Maneuver.TYPE_TURN_NORMAL_LEFT) "${direction}_valid" else "${direction}_not_valid"
|
||||
"right" -> if (stepData.maneuverType == Maneuver.TYPE_TURN_NORMAL_RIGHT) "${direction}_valid" else "${direction}_not_valid"
|
||||
"straight" -> if (stepData.maneuverType == Maneuver.TYPE_STRAIGHT) "${direction}_valid" else "${direction}_not_valid"
|
||||
"slight_right" -> if (stepData.maneuverType == Maneuver.TYPE_TURN_SLIGHT_RIGHT) "${direction}_valid" else "${direction}_not_valid"
|
||||
"slight_left" -> if (stepData.maneuverType == Maneuver.TYPE_TURN_SLIGHT_LEFT) "${direction}_valid" else "${direction}_not_valid"
|
||||
else -> {""}
|
||||
}
|
||||
}
|
||||
|
||||
fun resourceId(
|
||||
variableName: String,
|
||||
): Int {
|
||||
return when(variableName) {
|
||||
"left_not_valid" -> R.drawable.left_not_valid
|
||||
"left_valid" -> R.drawable.left_valid
|
||||
"left_valid_right_not_valid" -> R.drawable.left_valid_right_not_valid
|
||||
"right_not_valid" -> R.drawable.right_not_valid
|
||||
"right_valid" -> R.drawable.right_valid
|
||||
"slight_right_not_valid" -> R.drawable.slight_right_not_valid
|
||||
"slight_right_valid" -> R.drawable.slight_right_valid
|
||||
"straight_not_valid" -> R.drawable.straight_not_valid
|
||||
"straight_not_valid_right_valid" -> R.drawable.straight_not_valid_right_valid
|
||||
"straight_valid" -> R.drawable.straight_valid
|
||||
else -> {R.drawable.ic_close_white_24dp}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,11 +18,17 @@ import com.kouros.navigation.data.nominatim.Search
|
||||
import com.kouros.navigation.data.nominatim.SearchResult
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.navigation.data.overpass.Overpass
|
||||
import com.kouros.navigation.data.tomtom.Features
|
||||
import com.kouros.navigation.data.tomtom.Traffic
|
||||
import com.kouros.navigation.data.tomtom.TrafficData
|
||||
import com.kouros.navigation.utils.GeoUtils.createPointCollection
|
||||
import com.kouros.navigation.utils.Levenshtein
|
||||
import com.kouros.navigation.utils.NavigationUtils
|
||||
import com.kouros.navigation.utils.location
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.maplibre.geojson.FeatureCollection
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@@ -32,6 +38,11 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
val traffic: MutableLiveData<Map<String, String>> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
|
||||
val previewRoute: MutableLiveData<String> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
@@ -54,7 +65,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
val placeLocation: MutableLiveData<SearchResult> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
|
||||
val contactAddress: MutableLiveData<List<Place>> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
@@ -67,12 +78,14 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
val maxSpeed: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
val routingEngine: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
|
||||
fun loadRecentPlace(location: Location, context: Context) {
|
||||
fun loadRecentPlace(location: Location, carOrientation: Float, context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val placeBox = boxStore.boxFor(Place::class)
|
||||
@@ -84,11 +97,17 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
query.close()
|
||||
for (place in results) {
|
||||
val plLocation = location(place.longitude, place.latitude)
|
||||
val distance = repository.getRouteDistance(location, plLocation, SearchFilter(), context)
|
||||
val distance = repository.getRouteDistance(
|
||||
location,
|
||||
plLocation,
|
||||
carOrientation,
|
||||
SearchFilter(),
|
||||
context
|
||||
)
|
||||
place.distance = distance.toFloat()
|
||||
if (place.distance > 1F) {
|
||||
recentPlace.postValue(place)
|
||||
return@launch
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -97,7 +116,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRecentPlaces(context: Context, location: Location) {
|
||||
fun loadRecentPlaces(context: Context, location: Location, carOrientation: Float) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val placeBox = boxStore.boxFor(Place::class)
|
||||
@@ -109,15 +128,16 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
query.close()
|
||||
for (place in results) {
|
||||
val plLocation = location(place.longitude, place.latitude)
|
||||
if (place.latitude != 0.0) {
|
||||
val distance =
|
||||
repository.getRouteDistance(
|
||||
location,
|
||||
plLocation,
|
||||
getSearchFilter(context), context
|
||||
)
|
||||
place.distance = distance.toFloat()
|
||||
}
|
||||
if (place.latitude != 0.0) {
|
||||
val distance =
|
||||
repository.getRouteDistance(
|
||||
location,
|
||||
plLocation,
|
||||
carOrientation,
|
||||
getSearchFilter(context), context
|
||||
)
|
||||
place.distance = distance.toFloat()
|
||||
}
|
||||
}
|
||||
places.postValue(results)
|
||||
} catch (e: Exception) {
|
||||
@@ -126,7 +146,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFavorites(context: Context, location: Location) {
|
||||
fun loadFavorites(context: Context, location: Location, carOrientation: Float) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val placeBox = boxStore.boxFor(Place::class)
|
||||
@@ -139,7 +159,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
for (place in results) {
|
||||
val plLocation = location(place.longitude, place.latitude)
|
||||
val distance =
|
||||
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context)
|
||||
repository.getRouteDistance(
|
||||
location,
|
||||
plLocation,
|
||||
carOrientation,
|
||||
getSearchFilter(context),
|
||||
context
|
||||
)
|
||||
place.distance = distance.toFloat()
|
||||
}
|
||||
favorites.postValue(results)
|
||||
@@ -149,13 +175,20 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRoute(context: Context, currentLocation: Location, location: Location) {
|
||||
fun loadRoute(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
carOrientation: Float
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
route.postValue(
|
||||
repository.getRoute(
|
||||
context,
|
||||
currentLocation,
|
||||
location,
|
||||
carOrientation,
|
||||
getSearchFilter(context)
|
||||
)
|
||||
)
|
||||
@@ -165,13 +198,60 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location) {
|
||||
fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val data = repository.getTraffic(
|
||||
context,
|
||||
currentLocation,
|
||||
carOrientation
|
||||
)
|
||||
val trafficData = rebuildTraffic(data)
|
||||
traffic.postValue(
|
||||
trafficData
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun rebuildTraffic(data: String): Map<String, String> {
|
||||
val featureCollection = FeatureCollection.fromJson(data)
|
||||
val incidents = mutableMapOf<String, String>()
|
||||
val queuing = featureCollection.features()!!
|
||||
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
|
||||
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
|
||||
val stationary = featureCollection.features()!!
|
||||
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
|
||||
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
|
||||
val slow = featureCollection.features()!!
|
||||
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
|
||||
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
|
||||
val heavy = featureCollection.features()!!
|
||||
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
|
||||
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
|
||||
val roadworks = featureCollection.features()!!
|
||||
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
|
||||
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
|
||||
|
||||
return incidents
|
||||
}
|
||||
|
||||
fun loadPreviewRoute(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
carOrientation: Float
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
previewRoute.postValue(
|
||||
repository.getRoute(
|
||||
context,
|
||||
currentLocation,
|
||||
location,
|
||||
carOrientation,
|
||||
getSearchFilter(context)
|
||||
)
|
||||
)
|
||||
@@ -229,21 +309,24 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchPlaces(search: String, location: Location) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val placesJson = repository.searchPlaces(search, location)
|
||||
val gson = GsonBuilder().serializeNulls().create()
|
||||
val places = gson.fromJson(placesJson, Search::class.java)
|
||||
val distPlaces = mutableListOf<SearchResult>()
|
||||
places.forEach {
|
||||
val plLocation =
|
||||
location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble())
|
||||
val distance = plLocation.distanceTo(location)
|
||||
it.distance = distance
|
||||
distPlaces.add(it)
|
||||
if (placesJson.isNotEmpty()) {
|
||||
val gson = GsonBuilder().serializeNulls().create()
|
||||
val places = gson.fromJson(placesJson, Search::class.java)
|
||||
val distPlaces = mutableListOf<SearchResult>()
|
||||
places.forEach {
|
||||
val plLocation =
|
||||
location(longitude = it.lon.toDouble(), latitude = it.lat.toDouble())
|
||||
val distance = plLocation.distanceTo(location)
|
||||
it.distance = distance
|
||||
distPlaces.add(it)
|
||||
}
|
||||
val sortedList = distPlaces.sortedWith(compareBy { it.distance })
|
||||
searchPlaces.postValue(sortedList)
|
||||
}
|
||||
val sortedList = distPlaces.sortedWith(compareBy { it.distance })
|
||||
searchPlaces.postValue(sortedList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,13 +353,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun getSpeedCameras(location: Location, radius : Double) {
|
||||
fun getSpeedCameras(location: Location, radius: Double) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
|
||||
val distAmenities = mutableListOf<Elements>()
|
||||
amenities.forEach {
|
||||
val plLocation =
|
||||
location(longitude = it.lon!!, latitude = it.lat!!)
|
||||
location(longitude = it.lon, latitude = it.lat)
|
||||
val distance = plLocation.distanceTo(location)
|
||||
it.distance = distance.toDouble()
|
||||
distAmenities.add(it)
|
||||
@@ -286,10 +369,22 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun getMaxSpeed(location: Location) : List<Elements> {
|
||||
fun getMaxSpeed(location: Location, street: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val levenshtein = Levenshtein()
|
||||
val lineString = "${location.latitude},${location.longitude}"
|
||||
val amenities = Overpass().getAround(10, lineString)
|
||||
return amenities
|
||||
amenities.forEach {
|
||||
if (it.tags.name != null) {
|
||||
val distance =
|
||||
levenshtein.distance(it.tags.name!!, street)
|
||||
if (distance < 5) {
|
||||
val speed = it.tags.maxspeed.toInt()
|
||||
maxSpeed.postValue(speed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveFavorite(place: Place) {
|
||||
@@ -298,6 +393,12 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
|
||||
fun saveRecent(place: Place) {
|
||||
if (place.category == Constants.FUEL_STATION
|
||||
|| place.category == Constants.CHARGING_STATION
|
||||
|| place.category == Constants.PHARMACY
|
||||
) {
|
||||
return
|
||||
}
|
||||
place.category = Constants.RECENT
|
||||
savePlace(place)
|
||||
}
|
||||
@@ -356,7 +457,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
}
|
||||
|
||||
fun getSearchFilter(context: Context): SearchFilter {
|
||||
|
||||
val avoidMotorway = NavigationUtils.getBooleanKeyValue(
|
||||
context = context,
|
||||
Constants.AVOID_MOTORWAY
|
||||
@@ -365,14 +465,15 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
context = context,
|
||||
Constants.AVOID_TOLLWAY
|
||||
)
|
||||
return SearchFilter.Builder()
|
||||
.avoidMotorway(avoidMotorway)
|
||||
.avoidTollway(avoidTollway)
|
||||
.build()
|
||||
return SearchFilter(avoidMotorway, avoidTollway)
|
||||
}
|
||||
|
||||
|
||||
fun loadPlaces2(context: Context, location: Location): SnapshotStateList<Place?> {
|
||||
fun loadPlaces2(
|
||||
context: Context,
|
||||
location: Location,
|
||||
carOrientation: Float
|
||||
): SnapshotStateList<Place?> {
|
||||
val results = listOf<Place>()
|
||||
try {
|
||||
val placeBox = boxStore.boxFor(Place::class)
|
||||
@@ -385,7 +486,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
|
||||
for (place in results) {
|
||||
val plLocation = location(place.longitude, place.latitude)
|
||||
val distance =
|
||||
repository.getRouteDistance(location, plLocation, getSearchFilter(context), context)
|
||||
repository.getRouteDistance(
|
||||
location,
|
||||
plLocation,
|
||||
carOrientation,
|
||||
getSearchFilter(context),
|
||||
context
|
||||
)
|
||||
place.distance = distance.toFloat()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.kouros.navigation.utils
|
||||
|
||||
import android.location.Location
|
||||
import com.kouros.navigation.data.BoundingBox
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import org.maplibre.geojson.FeatureCollection
|
||||
@@ -10,7 +9,6 @@ import org.maplibre.spatialk.geojson.Feature
|
||||
import org.maplibre.spatialk.geojson.dsl.addFeature
|
||||
import org.maplibre.spatialk.geojson.dsl.buildFeatureCollection
|
||||
import org.maplibre.spatialk.geojson.dsl.buildLineString
|
||||
import org.maplibre.spatialk.geojson.dsl.buildMultiPoint
|
||||
import org.maplibre.spatialk.geojson.toJson
|
||||
import org.maplibre.turf.TurfMeasurement
|
||||
import org.maplibre.turf.TurfMisc
|
||||
@@ -33,9 +31,7 @@ object GeoUtils {
|
||||
return newLocation
|
||||
}
|
||||
|
||||
|
||||
fun decodePolyline(encoded: String, precision: Int = 6,): List<List<Double>> {
|
||||
//val precisionOptional = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6
|
||||
fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> {
|
||||
val factor = 10.0.pow(precision)
|
||||
|
||||
var lat = 0
|
||||
@@ -93,6 +89,7 @@ object GeoUtils {
|
||||
}
|
||||
|
||||
fun createLineStringCollection(lineCoordinates: List<List<Double>>): String {
|
||||
// return createPointCollection(lineCoordinates, "Route")
|
||||
val lineString = buildLineString {
|
||||
lineCoordinates.forEach {
|
||||
add(org.maplibre.spatialk.geojson.Point(
|
||||
@@ -118,40 +115,31 @@ object GeoUtils {
|
||||
return featureCollection.toJson()
|
||||
}
|
||||
|
||||
fun getOverpassBbox(location: Location, radius: Double): String {
|
||||
|
||||
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
|
||||
val neLon = bbox["ne"]?.get("lon")
|
||||
val neLat = bbox["ne"]?.get("lat")
|
||||
val swLon = bbox["sw"]?.get("lon")
|
||||
val swLat = bbox["sw"]?.get("lat")
|
||||
return "$swLon,$swLat,$neLon,$neLat"
|
||||
}
|
||||
/**
|
||||
* Calculate the lat and len of a square around a point.
|
||||
* @return latMin, latMax, lngMin, lngMax
|
||||
*/
|
||||
fun calculateSquareRadius(lat: Double, lng: Double, radius: Double): String {
|
||||
val earthRadius = 6371.0 // earth radius in km
|
||||
val latMin = lat - toDegrees(radius / earthRadius)
|
||||
val latMax = lat + toDegrees(radius / earthRadius)
|
||||
val lngMin = lng - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||
val lngMax = lng + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||
|
||||
fun getBoundingBox2(location: Location, radius: Double): BoundingBox {
|
||||
val bbox = getBoundingBox(location.longitude, location.latitude, radius)
|
||||
val neLon = bbox["ne"]?.get("lon")
|
||||
val neLat = bbox["ne"]?.get("lat")
|
||||
val swLon = bbox["sw"]?.get("lon")
|
||||
val swLat = bbox["sw"]?.get("lat")
|
||||
return BoundingBox(swLat ?: 0.0 , swLon ?: 0.0, neLat ?: 0.0, neLon ?: 0.0)
|
||||
return "$lngMin,$latMin,$lngMax,$latMax"
|
||||
}
|
||||
fun getBoundingBox(
|
||||
lat: Double,
|
||||
lon: Double,
|
||||
radius: Double
|
||||
): Map<String, Map<String, Double>> {
|
||||
): String {
|
||||
val earthRadius = 6371.0
|
||||
val maxLat = lat + toDegrees(radius / earthRadius)
|
||||
val minLat = lat - toDegrees(radius / earthRadius)
|
||||
val maxLon = lon + toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||
val minLon = lon - toDegrees(radius / earthRadius / cos(toRadians(lat)))
|
||||
|
||||
return mapOf(
|
||||
"nw" to mapOf("lat" to maxLat, "lon" to minLon),
|
||||
"ne" to mapOf("lat" to maxLat, "lon" to maxLon),
|
||||
"sw" to mapOf("lat" to minLat, "lon" to minLon),
|
||||
"se" to mapOf("lat" to minLat, "lon" to maxLon)
|
||||
)
|
||||
return "$minLat,$minLon,$maxLat,$maxLon"
|
||||
}
|
||||
}
|
||||
@@ -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 (Wagner–Fischer 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()
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.kouros.navigation.data.Constants.ROUTING_ENGINE
|
||||
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.osrm.OsrmRepository
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||
import com.kouros.navigation.model.ViewModel
|
||||
import java.time.LocalDateTime
|
||||
@@ -25,11 +26,12 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object NavigationUtils {
|
||||
|
||||
fun getRouteEngine(context: Context): ViewModel {
|
||||
fun getViewModel(context: Context): ViewModel {
|
||||
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
|
||||
return when (routeEngine) {
|
||||
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
|
||||
else -> ViewModel(OsrmRepository())
|
||||
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
|
||||
else -> ViewModel(TomTomRepository())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,16 +94,16 @@ fun calculateZoom(speed: Double?): Double {
|
||||
in 61..70 -> 16.5
|
||||
else -> 16.0
|
||||
}
|
||||
return zoom.toDouble()
|
||||
return zoom
|
||||
}
|
||||
|
||||
fun previewZoom(previewDistance: Double): Double {
|
||||
when (previewDistance) {
|
||||
in 0.0..10.0 -> return 13.0
|
||||
in 10.0..20.0 -> return 11.0
|
||||
in 20.0..30.0 -> return 10.0
|
||||
in 0.0..10.0 -> return 13.5
|
||||
in 10.0..20.0 -> return 11.5
|
||||
in 20.0..30.0 -> return 10.5
|
||||
}
|
||||
return 9.0
|
||||
return 9.5
|
||||
}
|
||||
|
||||
fun calculateTilt(newZoom: Double, tilt: Double): Double =
|
||||
|
||||
11
common/data/src/main/res/drawable/arrow_back_24px.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -6,5 +6,5 @@
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M337,746L425,606L372,606L372,501L285,641L337,641L337,746ZM220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM220,780L489,780L489,468L220,468L220,780ZM160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.71,468 649.36,482.64Q664,497.29 664,518L664,737Q664,759 681.5,773.5Q699,788 722,788Q745,788 765,773.5Q785,759 785,737L785,350L770,350Q757.25,350 748.63,341.37Q740,332.75 740,320L740,230L760,230L760,180L790,180L790,230L830,230L830,180L860,180L860,230L880,230L880,320Q880,332.75 871.38,341.37Q862.75,350 850,350L835,350L835,736.69Q835,780 801,810Q767,840 721.82,840Q677.66,840 645.83,810Q614,780 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM489,780L220,780L220,780L489,780Z"/>
|
||||
android:pathData="M220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.71,468 649.36,482.64Q664,497.29 664,518L664,737Q664,759 681.5,773.5Q699,788 722,788Q745,788 765,773.5Q785,759 785,737L785,350L770,350Q757.25,350 748.63,341.37Q740,332.75 740,320L740,230L760,230L760,180L790,180L790,230L830,230L830,180L860,180L860,230L880,230L880,320Q880,332.75 871.38,341.37Q862.75,350 850,350L835,350L835,736.69Q835,780 801,810Q767,840 721.82,840Q677.66,840 645.83,810Q614,780 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM337,746L425,606L372,606L372,501L285,641L337,641L337,746Z"/>
|
||||
</vector>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 914 B |
|
Before Width: | Height: | Size: 883 B After Width: | Height: | Size: 883 B |
BIN
common/data/src/main/res/drawable/left_o_straight_x.png
Normal file
|
After Width: | Height: | Size: 881 B |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
common/data/src/main/res/drawable/left_x_straight_o.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
10
common/data/src/main/res/drawable/menu_24px.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M120,720L120,640L840,640L840,720L120,720ZM120,520L120,440L840,440L840,520L120,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
|
||||
</vector>
|
||||
BIN
common/data/src/main/res/drawable/none.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 888 B After Width: | Height: | Size: 888 B |
|
Before Width: | Height: | Size: 895 B After Width: | Height: | Size: 895 B |