commit d63747e81170c1d200287bfe65f3186fe9be3342 Author: Dimitris Date: Thu Nov 13 19:46:08 2025 +0100 Initial commit diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..e1ad413 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,96 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.kouros.navigation" + compileSdk = 36 + + defaultConfig { + applicationId = "com.kouros.navigation" + minSdk = 34 + targetSdk = 36 + versionCode = 1 + versionName = "0.1.1" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + signingConfigs { + getByName("debug") { + keyAlias = "alias" + keyPassword = "alpha2000" + storeFile = file("/home/kouros/work/keystore/keystore") + storePassword = "alpha2000" + } + create("release") { + keyAlias = "alias" + keyPassword = "alpha2000" + storeFile = file("/home/kouros/work/keystore/keystore") + storePassword = "alpha2000" + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + // Specifies one flavor dimension. + flavorDimensions += "version" + productFlavors { + create("demo") { + dimension = "version" + applicationIdSuffix = ".demo" + versionNameSuffix = "-demo" + } + create("full") { + dimension = "version" + applicationIdSuffix = ".full" + versionNameSuffix = "-full" + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.ui) + implementation(libs.androidx.car.app) + implementation(libs.androidx.material3) + implementation(libs.androidx.runtime.livedata) + implementation(libs.koin.androidx.compose) + implementation(libs.maplibre.compose) + //implementation(libs.maplibre.composeMaterial3) + implementation(libs.accompanist.permissions) + + implementation(project(":common:data")) + implementation(project(":common:car")) + implementation(libs.play.services.location) + implementation(libs.androidx.compose.runtime) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + +} + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c8e87ce --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/MainActivity.kt b/app/src/main/java/com/kouros/navigation/MainActivity.kt new file mode 100644 index 0000000..ba09ff0 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/MainActivity.kt @@ -0,0 +1,352 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kouros.navigation + +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationManager +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.core.location.LocationListenerCompat +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.example.places.ui.theme.PlacesTheme +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.kouros.navigation.data.Category +import com.kouros.navigation.data.Constants +import com.kouros.navigation.data.Constants.TAG +import com.kouros.navigation.data.Constants.homeLocation +import com.kouros.navigation.data.NavigationRepository +import com.kouros.navigation.model.RouteModel +import com.kouros.navigation.model.ViewModel +import org.koin.androidx.compose.koinViewModel +import org.maplibre.compose.camera.CameraPosition +import org.maplibre.compose.camera.rememberCameraState +import org.maplibre.compose.expressions.dsl.const +import org.maplibre.compose.layers.FillLayer +import org.maplibre.compose.layers.LineLayer +import org.maplibre.compose.location.LocationPuck +import org.maplibre.compose.location.LocationPuckColors +import org.maplibre.compose.location.rememberDefaultLocationProvider +import org.maplibre.compose.location.rememberUserLocationState +import org.maplibre.compose.map.GestureOptions +import org.maplibre.compose.map.MapOptions +import org.maplibre.compose.map.MaplibreMap +import org.maplibre.compose.map.OrnamentOptions +import org.maplibre.compose.sources.GeoJsonData +import org.maplibre.compose.sources.getBaseSource +import org.maplibre.compose.sources.rememberGeoJsonSource +import org.maplibre.compose.style.BaseStyle +import org.maplibre.spatialk.geojson.Position +import kotlin.time.Duration.Companion.seconds + + +val geojson = MutableLiveData("") + +class MainActivity : ComponentActivity() { + + val vieModel = ViewModel(NavigationRepository()) + val routeModel = RouteModel() + + val observer = Observer { newRoute -> + routeModel.createNavigationRoute(newRoute) + geojson.value = routeModel.geoJson + homeLocation.latitude = 48.155782 + homeLocation.longitude = 11.607921 + } + + val cameraPosition = MutableLiveData( + CameraPosition( + zoom = 15.0, + target = Position(latitude = 48.1857475, longitude = 11.5793627) + ) + ) + + init { + vieModel.route.observe(this, observer) + vieModel.loadRoute( + homeLocation, + Constants.home2Location + ) + } + + var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? -> + updateLocation(location) + } + + + @SuppressLint("MissingPermission") + fun requestLocationUpdates() { + val locationManager = + getSystemService(LOCATION_SERVICE) as LocationManager + val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + updateLocation(location) + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + /* minTimeMs= */ 100, + /* minDistanceM= */ 0f, + mLocationListener + ) + } + + fun updateLocation(location: Location?) { + if (location != null) { + cameraPosition.postValue( + cameraPosition.value!!.copy( + zoom = 15.0, + target = Position(location.longitude, location.latitude), + ) + ) + } + } + + fun test() { + for (i in 0.. + Column(modifier = Modifier.padding(innerPadding)) { + CheckPermission() + } + } + } + } + } + + override fun onPause() { + super.onPause() + val locationManager = + getSystemService(LOCATION_SERVICE) as LocationManager + locationManager.removeUpdates(mLocationListener) + } + + @SuppressLint("PermissionLaunchedDuringComposition") + @OptIn(ExperimentalPermissionsApi::class) + @Composable + fun CheckPermission() { + var rationaleState by remember { + mutableStateOf(null) + } + + @OptIn(ExperimentalPermissionsApi::class) + val fineLocationPermissionState = rememberMultiplePermissionsState( + listOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.READ_CONTACTS, + ), + ) + + if (fineLocationPermissionState.allPermissionsGranted) { + Map() + } + // Show rationale dialog when needed + rationaleState?.run { PermissionRationaleDialog(rationaleState = this) } + + PermissionRequestButton( + isGranted = fineLocationPermissionState.allPermissionsGranted, + title = "Precise location access", + ) { + if (fineLocationPermissionState.shouldShowRationale) { + rationaleState = RationaleState( + "Request Precise Location", + "In order to use this feature please grant access by accepting " + "the location permission dialog." + "\n\nWould you like to continue?", + ) { proceed -> + if (proceed) { + fineLocationPermissionState.launchMultiplePermissionRequest() + } + rationaleState = null + } + } else { + fineLocationPermissionState.launchMultiplePermissionRequest() + } + } + } + + @Composable + fun Map() { + requestLocationUpdates() + val position: CameraPosition? by cameraPosition.observeAsState() + val geoJsonData: String? by geojson.observeAsState() + val cameraState = + rememberCameraState( + firstPosition = + CameraPosition( + target = Position( + position!!.target.latitude, + position!!.target.longitude + ), + zoom = 15.0, + ) + ) + + val locationProvider = rememberDefaultLocationProvider() + val locationState = rememberUserLocationState(locationProvider) + + MaplibreMap( + cameraState = cameraState, + //baseStyle = BaseStyle.Uri("https://tiles.openfreemap.org/styles/liberty"), + baseStyle = BaseStyle.Uri("https://kouros-online.de/liberty"), + options = + MapOptions( + gestureOptions = GestureOptions( + isTiltEnabled = true, + isZoomEnabled = true, + isRotateEnabled = false, + isScrollEnabled = true, + ), + ornamentOptions = OrnamentOptions( + isScaleBarEnabled = false + ) + ) + ) { + LocationPuck( + idPrefix = "user-location", + locationState = locationState, + cameraState = cameraState, + accuracyThreshold = 10f, + colors = LocationPuckColors(accuracyStrokeColor = Color.Green) + ) + getBaseSource(id = "openmaptiles")?.let { tiles -> + FillLayer(id = "example", visible = false, source = tiles, sourceLayer = "building") + RouteLayer(geoJsonData) + } + } + + LaunchedEffect(position) { + cameraState.animateTo( + finalPosition = CameraPosition( + bearing = position!!.bearing, + zoom = position!!.zoom, + target = position!!.target, + ), + duration = 3.seconds + ) + } + } + + @Composable + fun RouteLayer(geoJsonData: String?) { + val routes = + rememberGeoJsonSource(GeoJsonData.JsonString(geoJsonData!!)) + LineLayer( + id = "routes-casing", + source = routes, + color = const(Color.White), + width = const(6.dp), + ) + LineLayer( + id = "routes", + source = routes, + color = const(Color.Blue), + width = const(4.dp), + ) + } + + @Composable + fun PlaceList(viewModel: ViewModel = koinViewModel()) { + var categories: List + val places = viewModel.places.observeAsState().value ?: return + + val countries = places.groupBy { it.category }.map { + Category(id = Constants.RECENT, name = it.key!!) + } + categories = countries + val context = LocalContext.current + LazyColumn { + items(categories.size) { + val place = categories[it] + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .border( + 2.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(8.dp) + ) + .clip(RoundedCornerShape(8.dp)) + //.clickable { + //context.startActivity(place.toIntent(Intent.ACTION_VIEW)) + //} + .padding(8.dp) + ) { + Column { + Text( + text = place.name, + style = MaterialTheme.typography.labelLarge + ) + Text( + text = place.name, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + + } + } + } + } +} diff --git a/app/src/main/java/com/kouros/navigation/MainApplication.kt b/app/src/main/java/com/kouros/navigation/MainApplication.kt new file mode 100644 index 0000000..666f3f7 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/MainApplication.kt @@ -0,0 +1,26 @@ +package com.kouros.navigation + +import android.app.Application +import android.content.Context +import com.example.places.di.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin +import org.koin.core.logger.Level +class MainApplication : Application() { + + override fun onCreate() { + super.onCreate() + appContext = applicationContext + startKoin { + androidLogger(Level.DEBUG) + androidContext(this@MainApplication) + modules(appModule) + } + } + + companion object { + var appContext: Context? = null + private set + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/Permissions.kt b/app/src/main/java/com/kouros/navigation/Permissions.kt new file mode 100644 index 0000000..d440764 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/Permissions.kt @@ -0,0 +1,129 @@ +package com.kouros.navigation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.MultiplePermissionsState +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.rememberMultiplePermissionsState + +/** + * Simple screen that manages the location permission state + */ +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun Permissions(text: String, rationale: String, locationState: PermissionState) { + Permissions( + text = text, + rationale = rationale, + locationState = rememberMultiplePermissionsState( + permissions = listOf( + locationState.permission + ) + ) + ) +} + +/** + * Simple screen that manages the location permission state + */ +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun Permissions(text: String, rationale: String, locationState: MultiplePermissionsState) { + var showRationale by remember(locationState) { + mutableStateOf(false) + } + if (showRationale) { + PermissionRationaleDialog(rationaleState = RationaleState( + title = "Permission Access", + rationale = rationale, + onRationaleReply = { proceed -> + if (proceed) { + locationState.launchMultiplePermissionRequest() + } + showRationale = false + } + )) + } + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + PermissionRequestButton(isGranted = false, title = text) { + if (locationState.shouldShowRationale) { + showRationale = true + } else { + locationState.launchMultiplePermissionRequest() + } + } + } +} + +/** + * A button that shows the title or the request permission action. + */ +@Composable +fun PermissionRequestButton(isGranted: Boolean, title: String, onClick: () -> Unit) { + if (isGranted) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon(Icons.Outlined.CheckCircle, title, modifier = Modifier.size(48.dp)) + Spacer(Modifier.size(10.dp)) + Text(text = title, modifier = Modifier.background(Color.Transparent)) + } + } else { + Button(onClick = onClick) { + Text("Request $title") + } + } +} + +/** + * Simple AlertDialog that displays the given rationale state + */ +@Composable +fun PermissionRationaleDialog(rationaleState: RationaleState) { + AlertDialog(onDismissRequest = { rationaleState.onRationaleReply(false) }, title = { + Text(text = rationaleState.title) + }, text = { + Text(text = rationaleState.rationale) + }, confirmButton = { + TextButton(onClick = { + rationaleState.onRationaleReply(true) + }) { + Text("Continue") + } + }, dismissButton = { + TextButton(onClick = { + rationaleState.onRationaleReply(false) + }) { + Text("Dismiss") + } + }) +} + +data class RationaleState( + val title: String, + val rationale: String, + val onRationaleReply: (proceed: Boolean) -> Unit, +) \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/di/appModule.kt b/app/src/main/java/com/kouros/navigation/di/appModule.kt new file mode 100644 index 0000000..b18cf81 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/di/appModule.kt @@ -0,0 +1,12 @@ +package com.example.places.di + +import com.kouros.navigation.data.NavigationRepository +import com.kouros.navigation.model.ViewModel +import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val appModule = module { + viewModelOf(::ViewModel) + singleOf(::NavigationRepository) +} \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/ui/theme/Color.kt b/app/src/main/java/com/kouros/navigation/ui/theme/Color.kt new file mode 100644 index 0000000..4e9db57 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.places.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt b/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt new file mode 100644 index 0000000..fc06ad4 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt @@ -0,0 +1,57 @@ +package com.example.places.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +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.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +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), + */ +) + +@Composable +fun PlacesTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/ui/theme/Type.kt b/app/src/main/java/com/kouros/navigation/ui/theme/Type.kt new file mode 100644 index 0000000..c4d41ca --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.example.places.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + 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 + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..20357f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0afde1e --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Navigation + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..f5a8103 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/common/car/src/main/res/xml/automotive_app_desc.xml b/common/car/src/main/res/xml/automotive_app_desc.xml new file mode 100644 index 0000000..cec730a --- /dev/null +++ b/common/car/src/main/res/xml/automotive_app_desc.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt b/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt new file mode 100644 index 0000000..04ab17f --- /dev/null +++ b/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt @@ -0,0 +1,18 @@ +package com.kouros.navigation.car + +import com.kouros.navigation.car.navigation.RoutingModel +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/common/data/.gitignore b/common/data/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/common/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/data/build.gradle.kts b/common/data/build.gradle.kts new file mode 100644 index 0000000..8fd0328 --- /dev/null +++ b/common/data/build.gradle.kts @@ -0,0 +1,65 @@ +import org.gradle.kotlin.dsl.annotationProcessor + + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + kotlin("plugin.serialization") version "2.2.21" + kotlin("kapt") +} + + + +android { + namespace = "com.kouros.data" + compileSdk = 36 + + defaultConfig { + minSdk = 34 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation("io.insert-koin:koin-androidx-compose:4.1.1") + implementation("io.insert-koin:koin-core:4.1.1") + implementation("io.insert-koin:koin-android:4.1.1") + implementation("io.insert-koin:koin-compose-viewmodel:4.1.1") + + // objectbox + implementation("io.objectbox:objectbox-kotlin:5.0.1") + implementation(libs.androidx.material3) + annotationProcessor("io.objectbox:objectbox-processor:5.0.1") + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + + implementation(libs.maplibre.compose) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} + +apply(plugin = "io.objectbox") diff --git a/common/data/consumer-rules.pro b/common/data/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/common/data/objectbox-models/default.json b/common/data/objectbox-models/default.json new file mode 100644 index 0000000..90a8be7 --- /dev/null +++ b/common/data/objectbox-models/default.json @@ -0,0 +1,92 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "3:3556253001994555353", + "lastPropertyId": "10:2074102010889685023", + "name": "Place", + "properties": [ + { + "id": "1:8628248127112405947", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:5449777173432639670", + "name": "name", + "type": 9 + }, + { + "id": "4:7616797133438967216", + "name": "category", + "type": 9 + }, + { + "id": "5:4549304698322816870", + "name": "latitude", + "type": 8 + }, + { + "id": "6:5239710428443021248", + "name": "longitude", + "type": 8 + }, + { + "id": "7:6400677773087441740", + "name": "postalCode", + "type": 9 + }, + { + "id": "8:2520427138466605542", + "name": "city", + "type": 9 + }, + { + "id": "9:8470786445549505903", + "name": "street", + "type": 9 + }, + { + "id": "10:2074102010889685023", + "name": "distance", + "type": 7 + } + ], + "relations": [] + } + ], + "lastEntityId": "4:4849917137448238840", + "lastIndexId": "3:8164654097637798551", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [ + 5232739161494262087, + 1670248357005659634, + 4849917137448238840 + ], + "retiredIndexUids": [ + 1988419626350568402, + 5426976851182536573, + 8164654097637798551 + ], + "retiredPropertyUids": [ + 2083347946807922223, + 4259485286808072830, + 8424908988571282836, + 7365137158590972749, + 3637730979227083476, + 8954406503424388478, + 7467083398837280132, + 6885676442906238720, + 6365540590424804057, + 8979494032513344145, + 6908702166041138446 + ], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/common/data/objectbox-models/default.json.bak b/common/data/objectbox-models/default.json.bak new file mode 100644 index 0000000..fb11a20 --- /dev/null +++ b/common/data/objectbox-models/default.json.bak @@ -0,0 +1,113 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "3:3556253001994555353", + "lastPropertyId": "10:2074102010889685023", + "name": "Place", + "properties": [ + { + "id": "1:8628248127112405947", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:5449777173432639670", + "name": "name", + "type": 9 + }, + { + "id": "4:7616797133438967216", + "name": "category", + "type": 9 + }, + { + "id": "5:4549304698322816870", + "name": "latitude", + "type": 8 + }, + { + "id": "6:5239710428443021248", + "name": "longitude", + "type": 8 + }, + { + "id": "7:6400677773087441740", + "name": "postalCode", + "type": 9 + }, + { + "id": "8:2520427138466605542", + "name": "city", + "type": 9 + }, + { + "id": "9:8470786445549505903", + "name": "street", + "type": 9 + }, + { + "id": "10:2074102010889685023", + "name": "distance", + "type": 7 + } + ], + "relations": [] + }, + { + "id": "4:4849917137448238840", + "lastPropertyId": "3:6908702166041138446", + "name": "ObjectBoxTile", + "properties": [ + { + "id": "1:6365540590424804057", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:8979494032513344145", + "name": "key", + "indexId": "3:8164654097637798551", + "type": 9, + "flags": 2048 + }, + { + "id": "3:6908702166041138446", + "name": "bitmap", + "type": 23 + } + ], + "relations": [] + } + ], + "lastEntityId": "4:4849917137448238840", + "lastIndexId": "3:8164654097637798551", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [ + 5232739161494262087, + 1670248357005659634 + ], + "retiredIndexUids": [ + 1988419626350568402, + 5426976851182536573 + ], + "retiredPropertyUids": [ + 2083347946807922223, + 4259485286808072830, + 8424908988571282836, + 7365137158590972749, + 3637730979227083476, + 8954406503424388478, + 7467083398837280132, + 6885676442906238720 + ], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/common/data/proguard-rules.pro b/common/data/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/common/data/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/common/data/src/main/AndroidManifest.xml b/common/data/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/common/data/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/ManeuverType.kt b/common/data/src/main/java/com/kouros/navigation/data/ManeuverType.kt new file mode 100644 index 0000000..515bfab --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/ManeuverType.kt @@ -0,0 +1,48 @@ +package com.kouros.navigation.data + +enum class ManeuverType(val value: Int) { + None(0), + Start(1), + StartRight(2), + StartLeft(3), + Destination(4), + DestinationRight(5), + DestinationLeft(6), + Becomes(7), + Continue(8), + SlightRight(9), + Right(10), + SharpRight(11), + UturnRight(12), + UturnLeft(13), + SharpLeft(14), + Left(15), + SlightLeft(16), + RampStraight(17), + RampRight(18), + RampLeft(19), + ExitRight(20), + ExitLeft(21), + StayStraight(22), + StayRight(23), + StayLeft(24), + Merge(25), + RoundaboutEnter(26), + RoundaboutExit(27), + FerryEnter(28), + FerryExit(29), + Transit(30), + TransitTransfer(31), + TransitRemainOn(32), + TransitConnectionStart(33), + TransitConnectionTransfer(34), + TransitConnectionDestination(35), + PostTransitConnectionDestination(36), + MergeRight(37), + MergeLeft(38), + ElevatorEnter(39), + StepsEnter(40), + EscalatorEnter(41), + BuildingEnter(42), + BuildingExit(43), +} \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt new file mode 100644 index 0000000..b155fe9 --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kouros.navigation.data + +import android.location.Location +import org.json.JSONArray +import java.net.Authenticator +import java.net.HttpURLConnection +import java.net.PasswordAuthentication +import java.net.URL +import kotlinx.serialization.json.Json + + +class NavigationRepository { + + private val placesUrl = "https://kouros-online.de/maps/placespwd"; + + private val routeUrl = "https://kouros-online.de/valhalla/route?json=" + + fun getRoute(currentLocation : Location, location: Location): String { + val vLocation = listOf( + Locations(lat = currentLocation.latitude, lon = currentLocation.longitude), + Locations(lat = location.latitude, lon = location.longitude) + ) + val valhallaLocation = ValhallaLocation( + locations = vLocation, + costing = "auto", + units = "km", + id = "my_work_route", + language = "de-DE" + ) + val routeLocation = Json.encodeToString(valhallaLocation) + return fetchUrl(routeUrl + routeLocation) + } + + fun getPlaces(): List { + val places: MutableList = ArrayList() + val placesStr = fetchUrl(placesUrl) + val jArray = JSONArray(placesStr) + for (i in 0..() + /* Place( + id = 0, + name = "Vogelhartstr. 17", + category = "Favorites", + latitude = 48.1857475, + longitude = 11.5793627, + postalCode = "80807", + city = "München", + street = "Vogelhartstr. 17" + + ), + Place( + id = 0, + name = "Hohenwaldeckstr. 27", + category = "Recent", + latitude = 48.1165005, + longitude = 11.594349, + postalCode = "81541", + city = "München", + street = "Hohenwaldeckstr. 27", + ) +) */ + +// GeoJSON data classes +@Serializable +data class GeoJsonLineString( + val type: String, + val coordinates: List> +) + +@Serializable +data class GeoJsonFeature( + val type: String, + val geometry: GeoJsonLineString +) + +@Serializable +data class GeoJsonFeatureCollection( + val type: String, + val features: List +) + +@Serializable +data class Locations ( + var lat : Double, + var lon : Double, + var street : String = "" + +) + +@Serializable +data class ValhallaLocation ( + var locations: List, + var costing: String, + var units: String, + var id: String, + var language: String +) + +object Constants { + + const val TAG: String = "Navigation" + + const val CONTACTS: String = "Contacts" + + const val RECENT: String = "Recent" + + /** 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 + } +} + + diff --git a/common/data/src/main/java/com/kouros/navigation/model/Contacts.kt b/common/data/src/main/java/com/kouros/navigation/model/Contacts.kt new file mode 100644 index 0000000..d45e604 --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/model/Contacts.kt @@ -0,0 +1,72 @@ +package com.kouros.navigation.model + +import android.annotation.SuppressLint +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import android.provider.ContactsContract +import com.kouros.navigation.data.ContactData + + +class Contacts(private var context: Context) { + + @SuppressLint("Range") + fun retrieveContacts(): List { + val contentResolver = context.contentResolver + val projection: Array = arrayOf( + ContactsContract.Data.CONTACT_ID, + ContactsContract.Data.MIMETYPE, + ContactsContract.Data.DATA1, + ContactsContract.Data.DATA2, + ContactsContract.Contacts.DISPLAY_NAME, + ) + val cursor = contentResolver.query( + ContactsContract.Data.CONTENT_URI, + projection, + null, + null, + null + ) + var address = "" + val result: MutableList = mutableListOf() + cursor?.apply { + while (moveToNext()) { + val contactId = getLong(getColumnIndex(ContactsContract.Data.CONTACT_ID)) + val name = getString(getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)) + if (name.contains("Jola") || name.contains("Dominic") + || name.contains("Μεντή") + || name.contains("David")) { + val mimeType: String = getString(getColumnIndex(ContactsContract.Data.MIMETYPE)) + if (mimeType == ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE) { + address = getString(getColumnIndex(ContactsContract.Data.DATA1)) + val avatar = retrieveAvatar(context, contactId) + result.add(ContactData(contactId, name, address, avatar)) + } + } + } + } + cursor?.close() + return result + } + + private fun retrieveAvatar(context: Context, contactId: Long): Uri? { + return context.contentResolver.query( + ContactsContract.Data.CONTENT_URI, + null, + "${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} = '${ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'", + arrayOf(contactId.toString()), + null + )?.use { + if (it.moveToFirst()) { + val contactUri = ContentUris.withAppendedId( + ContactsContract.Contacts.CONTENT_URI, + contactId + ) + Uri.withAppendedPath( + contactUri, + ContactsContract.Contacts.Photo.CONTENT_DIRECTORY + ) + } else null + } + } +} \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt new file mode 100644 index 0000000..633ad80 --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -0,0 +1,206 @@ +package com.kouros.navigation.model + +import android.location.Location +import android.location.LocationManager +import com.kouros.navigation.data.Place +import com.kouros.navigation.utils.NavigationUtils.Utils.createGeoJson +import com.kouros.navigation.utils.NavigationUtils.Utils.decodePolyline +import org.json.JSONArray +import org.json.JSONObject +import kotlin.math.roundToInt + + +open class RouteModel { + + var polylineLocations: List> = emptyList() + + lateinit var maneuvers: JSONArray + lateinit var locations: JSONArray + lateinit var summary: JSONObject + + lateinit var destination: Place + var navigating = false + + var arrived = false + + var maneuverIndex = 0 + + var maneuverType = 0 + + var currentIndex = 0 + + var distanceToStepEnd = 0F + + var beginIndex = 0 + + var endIndex = 0 + var routingManeuvers = mutableListOf() + + var distanceToRoute = 0F + + var geoJson = "" + + private fun decodeValhallaRoute(route: String) { + if (route.isEmpty() || route == "[]") { + return; + } + val jObject = JSONObject(route) + val trip = jObject.getJSONObject("trip") + locations = trip.getJSONArray("locations") + val legs = trip.getJSONArray("legs") + summary = trip.getJSONObject("summary") + maneuvers = legs.getJSONObject(0).getJSONArray("maneuvers") + val shape = legs.getJSONObject(0).getString("shape") + polylineLocations = decodePolyline(shape) + } + + fun createNavigationRoute(route: String) { + decodeValhallaRoute(route) + for (i in 0.. 0) { + val maneuver = routingManeuvers[maneuverIndex] + val curTime = maneuver.getDouble("time") + val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex) + val time = curTime * percent / 100 + timeLeft += time + } + return timeLeft + } + + /** Returns the current [Step] left distance in km. */ + fun leftStepDistance(): Double { + val maneuver = routingManeuvers[maneuverIndex] + var leftDistance = maneuver.getDouble("length") + if (endIndex > 0) { + val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex) + //leftDistance = leftDistance * percent / 100 + leftDistance = (distanceToStepEnd / 1000).toDouble() + } + return leftDistance + } + + fun travelLeftDistance(): Double { + var leftDistance = 0.0 + for (i in maneuverIndex + 1.. 0) { + val maneuver = routingManeuvers[maneuverIndex] + val curDistance = maneuver.getDouble("length") + val percent = 100 * (endIndex - currentIndex) / (endIndex - beginIndex) + val time = curDistance * percent / 100 + leftDistance += time + } + return leftDistance + } + + fun isNavigating(): Boolean { + return navigating + } + + fun isArrived(): Boolean { + return arrived + } + + fun stopNavigating() { + navigating = false + polylineLocations = mutableListOf() + routingManeuvers = mutableListOf() + geoJson = "" + maneuverIndex = 0 + currentIndex = 0 + distanceToStepEnd = 0F + distanceToRoute = 0F + beginIndex = 0 + endIndex = 0 + + } +} \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt new file mode 100644 index 0000000..7b791d3 --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt @@ -0,0 +1,150 @@ +package com.kouros.navigation.model + +import android.content.Context +import android.location.Geocoder +import android.location.Location +import android.location.LocationManager +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kouros.navigation.data.Constants +import com.kouros.navigation.data.NavigationRepository +import com.kouros.navigation.data.ObjectBox.boxStore +import com.kouros.navigation.data.Place +import com.kouros.navigation.data.Place_ +import io.objectbox.kotlin.boxFor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ViewModel(private val repository: NavigationRepository) : ViewModel() { + + val route: MutableLiveData by lazy { + MutableLiveData() + } + + val previewRoute: MutableLiveData by lazy { + MutableLiveData() + } + + val places: MutableLiveData> by lazy { + MutableLiveData>() + } + + val contactAddress: MutableLiveData> by lazy { + MutableLiveData>() + } + + + fun loadPlaces(location: Location) { + viewModelScope.launch(Dispatchers.IO) { + try { + val pl = mutableListOf() + val placeBox = boxStore.boxFor(Place::class) + pl.addAll(placeBox.all) + for (place in pl) { + val plLocation = Location(LocationManager.GPS_PROVIDER) + plLocation.longitude = place.longitude + plLocation.latitude = place.latitude + val distance: Float = location.distanceTo(plLocation) + place.distance = distance / 1000 + } + places.postValue(pl) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun loadRoute(currentLocation: Location, location: Location) { + viewModelScope.launch(Dispatchers.IO) { + try { + route.postValue(repository.getRoute(currentLocation, location)) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun loadPreviewRoute(currentLocation: Location, location: Location) { + viewModelScope.launch(Dispatchers.IO) { + try { + previewRoute.postValue(repository.getRoute(currentLocation, location)) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun loadContacts(context: Context) { + viewModelScope.launch(Dispatchers.IO) { + try { + val geocoder = Geocoder(context) + val contactList = mutableListOf() + val contacts = Contacts(context = context) + val addresses = contacts.retrieveContacts() + for (address in addresses) { + val lines = address.address.split("\n") + geocoder.getFromLocationName( + address.address, 5) { + for (adr in it) { + contactList.add( + Place( + id = address.contactId, + name = address.name + " " + lines[0] + " " + lines[1], + Constants.CONTACTS, + street = lines[0], + city = lines[1], + latitude = adr.latitude, + longitude = adr.longitude, + avatar = address.avatar + ) + ) + } + contactAddress.postValue(contactList) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun saveRecent(place: Place) { + viewModelScope.launch(Dispatchers.IO) { + place.category = Constants.RECENT + try { + val placeBox = boxStore.boxFor(Place::class) + val query = placeBox + .query(Place_.name.equal(place.name!!)) + .build() + val results = query.find() + query.close() + if (results.isEmpty()) { + placeBox.put(place) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun deleteRecent(place: Place) { + viewModelScope.launch(Dispatchers.IO) { + place.category = Constants.RECENT + try { + val placeBox = boxStore.boxFor(Place::class) + val query = placeBox + .query(Place_.name.equal(place.name!!)) + .build() + val results = query.find() + query.close() + if (results.isNotEmpty()) { + placeBox.remove(place) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} + diff --git a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt new file mode 100644 index 0000000..26e8f2c --- /dev/null +++ b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt @@ -0,0 +1,108 @@ +package com.kouros.navigation.utils + +import android.location.Location +import android.location.LocationManager +import com.kouros.navigation.data.GeoJsonFeature +import com.kouros.navigation.data.GeoJsonFeatureCollection +import com.kouros.navigation.data.GeoJsonLineString +import kotlinx.serialization.json.Json +import java.lang.Math.toDegrees +import java.lang.Math.toRadians +import kotlin.math.asin +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin + +class NavigationUtils() { + object Utils { + fun decodePolyline(encoded: String, vararg precisionOptional: Int): List> { + val precision = if (precisionOptional.isNotEmpty()) precisionOptional[0] else 6 + val factor = 10.0.pow(precision) + + var lat = 0 + var lng = 0 + val coordinates = mutableListOf>() + var index = 0 + + while (index < encoded.length) { + var byte = 0x20 + var shift = 0 + var result = 0 + while (byte >= 0x20) { + byte = encoded[index].code - 63 + result = result or ((byte and 0x1f) shl shift) + shift += 5 + index++ + } + lat += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) + + byte = 0x20 + shift = 0 + result = 0 + while (byte >= 0x20) { + byte = encoded[index].code - 63 + result = result or ((byte and 0x1f) shl shift) + shift += 5 + index++ + } + lng += if ((result and 1) > 0) (result shr 1).inv() else (result shr 1) + coordinates.add(listOf(lng.toDouble() / factor, lat.toDouble() / factor)) + } + + return coordinates + } + + fun createGeoJson(lineCoordinates: List>): String { + val lineString = GeoJsonLineString(type = "LineString", coordinates = lineCoordinates) + val feature = GeoJsonFeature(type = "Feature", geometry = lineString) + val featureCollection = + GeoJsonFeatureCollection(type = "FeatureCollection", features = listOf(feature)) + val jsonString = Json.Default.encodeToString(featureCollection) + return jsonString + } + + fun getBoundingBox( + lat: Double, + lon: Double, + radius: Double + ): Map> { + val earthRadius = 6371.0 + val maxLat = lat + Math.toDegrees(radius / earthRadius) + val minLat = lat - Math.toDegrees(radius / earthRadius) + val maxLon = lon + Math.toDegrees(radius / earthRadius / cos(Math.toRadians(lat))) + val minLon = lon - Math.toDegrees(radius / earthRadius / cos(Math.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) + ) + } + + fun computeOffset(from: Location, distance: Double, heading: Double): Location { + val earthRadius = 6371009.0 + var distance = distance + var heading = heading + distance /= earthRadius + heading = toRadians(heading) + val fromLat: Double = toRadians(from.latitude) + val fromLng: Double = toRadians(from.longitude) + val cosDistance: Double = cos(distance) + val sinDistance = sin(distance) + val sinFromLat = sin(fromLat) + val cosFromLat: Double = cos(fromLat) + val sinLat: Double = cosDistance * sinFromLat + sinDistance * cosFromLat * cos(heading) + val dLng: Double = atan2( + sinDistance * cosFromLat * sin(heading), + cosDistance - sinFromLat * sinLat + ) + val snap = Location(LocationManager.GPS_PROVIDER) + snap.latitude = toDegrees(asin(sinLat)) + snap.longitude = toDegrees(fromLng + dLng) + return snap + //return LatLng(toDegrees(asin(sinLat)), toDegrees(fromLng + dLng)) + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..8d021d3 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,54 @@ +[versions] +agp = "8.13.1" +gradle = "8.13.1" +koinAndroidxCompose = "4.1.1" +kotlin = "2.2.21" +coreKtx = "1.17.0" +junit = "4.13.2" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +lifecycleRuntimeKtx = "2.9.4" +composeBom = "2025.11.00" +appcompat = "1.7.1" +material = "1.13.0" +carApp = "1.7.0" +#objectboxKotlin = "5.0.1" +ui = "1.9.4" +material3 = "1.4.0" +runtimeLivedata = "1.9.4" +foundation = "1.9.4" +maplibre-compose = "0.12.1" +playServicesLocation = "21.3.0" +runtime = "1.9.4" +accompanist = "0.32.0" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroidxCompose" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" } +#objectbox-kotlin = { module = "io.objectbox:objectbox-kotlin", version.ref = "objectboxKotlin" } +ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } +maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" } +maplibre-composeMaterial3 = { module = "org.maplibre.compose:maplibre-compose-material3", version = "maplibre-compose" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } +play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" } +androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "agp" } + diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..f1a2a89 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,33 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Navigation" + +include( + ":", + ":app", + ":common", + ":common:data", + ":common:car", +) + + +