diff --git a/app/build.gradle b/app/build.gradle
index d880b89..2121d65 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -81,6 +81,40 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
implementation 'io.coil-kt:coil-compose:2.7.0'
+ implementation 'org.maplibre.gl:android-sdk:13.0.2'
+ implementation 'com.google.maps.android:android-maps-utils:3.8.2'
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'com.google.android.material:material:1.11.0'
+ implementation 'androidx.activity:activity:1.8.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ implementation 'org.locationtech.jts:jts-core:1.20.0'
+ implementation 'com.squareup.retrofit2:retrofit:2.9.0'
+ implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
+ implementation 'org.maplibre.gl:android-sdk:13.0.2'
+ implementation 'com.google.android.gms:play-services-location:21.3.0'
+ implementation 'com.graphhopper:graphhopper-core:11.0'
+ implementation 'com.google.maps.android:android-maps-utils:3.4.0'
+ implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
+ implementation 'com.google.maps.android:android-maps-utils:3.8.2'
+
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
+
+ implementation 'org.maplibre.gl:android-sdk:11.5.1'
+}
+
+configurations.all {
+ resolutionStrategy {
+ force 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23'
+ force 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.23'
+ force 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.23'
+ force 'org.jetbrains.kotlin:kotlin-stdlib-common:1.9.23'
+ }
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6f8049c..f0b2ff6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,15 +11,20 @@
android:supportsRtl="true"
android:theme="@style/Theme.GoodRoad"
android:usesCleartextTraffic="true">
+
-
+
+
+
-
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/MapActivity.kt b/app/src/main/java/com/example/goodroad/MapActivity.kt
new file mode 100644
index 0000000..a5fa7c7
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/MapActivity.kt
@@ -0,0 +1,286 @@
+package com.example.goodroad
+
+import android.location.Address
+import org.maplibre.android.camera.CameraUpdateFactory
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.lifecycleScope
+import com.example.goodroad.features.location.LocationTracker
+import kotlinx.coroutines.launch
+import org.maplibre.android.MapLibre
+import org.maplibre.android.maps.MapView
+import org.maplibre.android.maps.Style
+import android.location.Geocoder
+import android.widget.Button
+import android.widget.EditText
+import android.widget.Toast
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.util.Locale
+import com.example.goodroad.model.RouteRequest
+import com.example.goodroad.model.RouteResponse
+import com.example.goodroad.features.network.api.GoodRoadApi
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import com.example.goodroad.features.network.utils.decodePoints
+import com.example.goodroad.model.PathResponse
+import org.maplibre.android.geometry.LatLng
+import com.google.maps.android.PolyUtil
+import org.maplibre.android.maps.MapLibreMap
+import org.maplibre.android.style.layers.LineLayer
+import org.maplibre.android.style.sources.GeoJsonSource
+import org.maplibre.android.style.layers.PropertyFactory
+import com.example.goodroad.data.network.ApiClient
+import com.example.goodroad.data.obstacle.ObstacleApi
+import kotlin.collections.firstOrNull
+import android.Manifest
+import android.content.pm.PackageManager
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+
+class MapActivity : AppCompatActivity() {
+ private lateinit var mapView: MapView
+ private lateinit var locationTracker: LocationTracker
+ private lateinit var addressEditText: EditText
+ private lateinit var setDestinationButton: Button
+
+ companion object {
+ private const val LOCATION_PERMISSION_REQUEST_CODE = 1001
+ }
+ private var startLat: Double = 0.0
+ private var startLon: Double = 0.0
+
+ private val api: GoodRoadApi by lazy {
+ ApiClient.routeApi
+ }
+
+ private lateinit var backButton: Button
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ supportActionBar?.hide()
+ MapLibre.getInstance(this)
+ setContentView(R.layout.activity_map)
+
+ mapView = findViewById(R.id.mapView)
+ mapView.onCreate(savedInstanceState)
+
+ addressEditText = findViewById(R.id.addressEditText)
+ setDestinationButton = findViewById(R.id.setDestinationButton)
+
+ locationTracker = LocationTracker(this)
+
+ mapView.getMapAsync { map ->
+ map.setStyle(Style.Builder().fromUri("https://tiles.openfreemap.org/styles/positron")) {
+ if (hasLocationPermission()) {
+ getUserLocation()
+ } else {
+ requestLocationPermission()
+ }
+ }
+ }
+
+ setDestinationButton.setOnClickListener {
+ val destinationAddress = addressEditText.text.toString()
+ if (destinationAddress.isNotBlank()) {
+ getCoordinatesFromAddress(destinationAddress)
+ } else {
+ Toast.makeText(this, "Введите адрес назначения", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ backButton = findViewById(R.id.backButton)
+ backButton.setOnClickListener {
+ finish()
+ }
+
+ }
+
+ private fun getUserLocation() {
+ lifecycleScope.launch {
+ val location = locationTracker.getCurrentLocation()
+ if (location != null) {
+ startLat = location.latitude
+ startLon = location.longitude
+ Toast.makeText(
+ this@MapActivity,
+ "Ваше местоположение: $startLat, $startLon",
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(this@MapActivity, "Не удалось определить местоположение", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ private fun getCoordinatesFromAddress(address: String) {
+ lifecycleScope.launch {
+ Toast.makeText(this@MapActivity, "Поиск адреса...", Toast.LENGTH_SHORT).show()
+
+ val addresses = withContext(Dispatchers.IO) {
+ try {
+ val geocoder = Geocoder(this@MapActivity, Locale.getDefault())
+ geocoder.getFromLocationName(address, 1)
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ if (!addresses.isNullOrEmpty()) {
+ val destination = addresses[0]
+ val endLat = destination.latitude
+ val endLon = destination.longitude
+
+ Toast.makeText(
+ this@MapActivity,
+ "Маршрут от \$startLat,\$startLon до \$endLat,\$endLon",
+ Toast.LENGTH_LONG
+ ).show()
+
+ buildRoute(endLat, endLon)
+ } else {
+ Toast.makeText(this@MapActivity, "Адрес не найден. Попробуйте точнее.", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ private fun buildRoute(endLat: Double, endLon: Double) {
+ lifecycleScope.launch {
+ //взять реальный Id пользователя
+ if (startLat == 0.0 || startLon == 0.0) {
+ Toast.makeText(this@MapActivity, "Стартовая точка не определена", Toast.LENGTH_SHORT).show()
+ return@launch
+ }
+
+ val res = ApiClient.obstacleApi.getUserObstaclePolicies()
+ val policies = res.body()
+ val allowedTypes = setOf("SAND", "GRAVEL")
+
+ if(policies != null) {
+ val request = RouteRequest(
+ start = "$startLat,$startLon",
+ end = "$endLat,$endLon",
+ avoidStairs = policies.find { it.obstacleType == "STAIRS" }?.selected == true,
+ maxCurbHeight = policies.find { it.obstacleType == "CURB" }?.maxAllowedSeverity?.toInt(),
+ maxSlopeAngle = policies.find { it.obstacleType == "ROAD_SLOPE" }?.maxAllowedSeverity?.toDouble(),
+ avoidBadRoad = policies.find { it.obstacleType == "POTHOLES" }?.selected == true,
+ avoidSurfaceTypes = policies.filter { it.selected && it.obstacleType in allowedTypes }
+ .map { it.obstacleType }
+ )
+
+ //drawRoute(RouteResponse(id = "test", paths = emptyList()))
+
+ try {
+ val response = api.getRoute(request)
+ drawRoute(response)
+ } catch (e: Exception) {
+ Toast.makeText(this@MapActivity, "Ошибка: ${e.message}", Toast.LENGTH_SHORT)
+ .show()
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+
+ private fun drawRoute(response: RouteResponse) {
+ val path = response.paths.firstOrNull()
+ if (path == null) {
+ Toast.makeText(this, "Маршрут не найден", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val points = decodePoints(path.points)
+ if (points.isEmpty()) {
+ Toast.makeText(this, "Нет точек для отрисовки", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val latLngs = points.map { LatLng(it.latitude, it.longitude) }
+
+ mapView.getMapAsync { map ->
+ map.getStyle { style ->
+ style.removeLayer("route-layer")
+ style.removeSource("route-source")
+
+ // Создаём GeoJSON строку вручную
+ val coordinates = latLngs.joinToString(", ") {
+ "[${it.longitude}, ${it.latitude}]"
+ }
+ val geojson = """
+ {
+ "type": "FeatureCollection",
+ "features": [{
+ "type": "Feature",
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [$coordinates]
+ }
+ }]
+ }
+ """.trimIndent()
+
+ val source = GeoJsonSource("route-source", geojson)
+ style.addSource(source)
+
+ val lineLayer = LineLayer("route-layer", "route-source").apply {
+ setProperties(
+ PropertyFactory.lineColor("#8B7AC6"),
+ PropertyFactory.lineWidth(6f),
+ PropertyFactory.lineOpacity(0.9f)
+ )
+ }
+ style.addLayer(lineLayer)
+
+ if (latLngs.isNotEmpty()) {
+ map.animateCamera(
+ CameraUpdateFactory.newLatLngZoom(latLngs.first(), 14.0),
+ 1000
+ )
+ }
+ }
+ }
+ }
+
+ private fun hasLocationPermission(): Boolean {
+ return ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun requestLocationPermission() {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
+ LOCATION_PERMISSION_REQUEST_CODE
+ )
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ getUserLocation()
+ } else {
+ Toast.makeText(this, "Без разрешения геолокация не будет работать", Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ override fun onStart() { super.onStart(); mapView.onStart() }
+ override fun onResume() { super.onResume(); mapView.onResume() }
+ override fun onPause() { super.onPause(); mapView.onPause() }
+ override fun onStop() { super.onStop(); mapView.onStop() }
+ override fun onDestroy() { super.onDestroy(); mapView.onDestroy() }
+ override fun onLowMemory() { super.onLowMemory(); mapView.onLowMemory() }
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ mapView.onSaveInstanceState(outState)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/data/network/ApiClient.kt b/app/src/main/java/com/example/goodroad/data/network/ApiClient.kt
index d3ea79b..dbd40d3 100644
--- a/app/src/main/java/com/example/goodroad/data/network/ApiClient.kt
+++ b/app/src/main/java/com/example/goodroad/data/network/ApiClient.kt
@@ -10,6 +10,7 @@ import okhttp3.logging.*
import retrofit2.*
import retrofit2.converter.gson.*
import java.util.concurrent.*
+import com.example.goodroad.features.network.api.GoodRoadApi
object ApiClient {
@@ -75,4 +76,8 @@ object ApiClient {
val reviewApi: ReviewApi by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
retrofit().create(ReviewApi::class.java)
}
+
+ val routeApi: GoodRoadApi by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
+ retrofit().create(GoodRoadApi::class.java)
+ }
}
diff --git a/app/src/main/java/com/example/goodroad/domain/model/LocationPoint.kt b/app/src/main/java/com/example/goodroad/domain/model/LocationPoint.kt
new file mode 100644
index 0000000..71dea23
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/domain/model/LocationPoint.kt
@@ -0,0 +1,9 @@
+package com.example.goodroad.domain.model
+
+data class LocationPoint(
+ val latitude: Double,
+ val longitude: Double,
+ val timestamp: Long? = null
+) {
+ fun ToLatLanString() : String = "$latitude,$longitude"
+}
diff --git a/app/src/main/java/com/example/goodroad/domain/model/Obstacles.kt b/app/src/main/java/com/example/goodroad/domain/model/Obstacles.kt
new file mode 100644
index 0000000..336bdef
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/domain/model/Obstacles.kt
@@ -0,0 +1,36 @@
+package com.example.goodroad.domain.model
+
+data class Obstacle(
+ val id: String,
+ val position: LocationPoint,
+ val type: ObstacleType,
+ val details: ObstacleDetails
+)
+
+enum class ObstacleType{
+ CURB,
+ STAIRS,
+ ROAD_SLOPE,
+ POTHOLES,
+ SAND,
+ GRAVEL;
+}
+
+sealed class ObstacleDetails {
+
+ data class Stairs(
+ val stepCount: Int,
+ val hasRamp: Boolean = false
+ ) : ObstacleDetails()
+
+ data class Slope(
+ val angleDegrees: Double,
+ ) : ObstacleDetails()
+
+ data class Curb(
+ val heightCm: Int,
+ val hasRampCut: Boolean = false
+ ) : ObstacleDetails()
+
+}
+
diff --git a/app/src/main/java/com/example/goodroad/features/location/LocationErrorHandler.kt b/app/src/main/java/com/example/goodroad/features/location/LocationErrorHandler.kt
new file mode 100644
index 0000000..ceabe97
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/features/location/LocationErrorHandler.kt
@@ -0,0 +1,76 @@
+package com.example.goodroad.features.location
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import androidx.appcompat.app.AlertDialog
+import com.google.android.gms.common.api.ResolvableApiException
+import com.google.android.gms.location.LocationRequest
+import com.google.android.gms.location.LocationServices
+import com.google.android.gms.location.LocationSettingsRequest
+import com.google.android.gms.location.Priority
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+import com.example.goodroad.features.location.LocationError
+import android.net.Uri
+
+class LocationErrorHandler (
+ private val context: Context
+) {
+ fun GetErrorMessage(error: LocationError): String {
+ return when(error) {
+ is LocationError.PermissionsDenied -> {
+ "Нет разрешения на доступ геолокации. Разрешите доступ в настройках"
+ }
+ is LocationError.GPSDisabled -> {
+ "GPS выключен."
+ }
+ is LocationError.NoLocation -> {
+ "Не удалось определить мостоположение. Попробуйте позже"
+ }
+ }
+ }
+
+ fun ShowErrorDialog(
+ error: LocationError,
+ onPositiveClick: (() -> Unit)? = null
+ ) {
+ val message = GetErrorMessage(error)
+
+ val builder = AlertDialog.Builder(context)
+ .setTitle("Ошибка геолокации")
+ .setMessage(message)
+ .setPositiveButton("ОК") {_, _ ->
+ onPositiveClick?.invoke()
+ }
+ when (error) {
+ is LocationError.PermissionsDenied -> {
+ builder.setNeutralButton("Настройки") { _, _ ->
+ openAppSettings()
+ }
+ }
+ is LocationError.GPSDisabled -> {
+ builder.setNeutralButton("Настройки GPS") { _, _ ->
+ openGpsSettings()
+ }
+ }
+ is LocationError.NoLocation -> {
+ }
+ }
+
+ builder.show()
+ }
+
+ private fun openAppSettings() {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:${context.packageName}")
+ context.startActivity(intent)
+ }
+
+ private fun openGpsSettings() {
+ val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
+ context.startActivity(intent)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/features/location/LocationTracker.kt b/app/src/main/java/com/example/goodroad/features/location/LocationTracker.kt
new file mode 100644
index 0000000..0392141
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/features/location/LocationTracker.kt
@@ -0,0 +1,143 @@
+package com.example.goodroad.features.location
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import android.location.Location
+import androidx.core.content.ContextCompat
+import com.example.goodroad.domain.model.LocationPoint
+import com.google.android.gms.location.FusedLocationProviderClient
+import com.google.android.gms.location.LocationCallback
+import com.google.android.gms.location.LocationRequest
+import com.google.android.gms.location.LocationResult
+import com.google.android.gms.location.LocationServices
+import com.google.android.gms.location.Priority
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+import android.os.Looper
+
+sealed class LocationError: Exception() {
+ object PermissionsDenied: LocationError()
+ object GPSDisabled: LocationError()
+ object NoLocation: LocationError()
+}
+
+class LocationTracker (
+ private val ctx: Context
+) {
+ private val fusedLocationClient: FusedLocationProviderClient =
+ LocationServices.getFusedLocationProviderClient(ctx)
+
+ fun hasPermissions(): Boolean {
+ return ContextCompat.checkSelfPermission(
+ ctx,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED ||
+ ContextCompat.checkSelfPermission(
+ ctx,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ suspend fun getCurrentLocation() : LocationPoint? {
+ if (!hasPermissions()) return null
+
+ return suspendCancellableCoroutine { continuation ->
+ try {
+ fusedLocationClient.lastLocation
+ .addOnSuccessListener { location ->
+ if (location != null) {
+ continuation.resume(location.toLocationPoint())
+ } else {
+ requestFreshLocation { freshLocation ->
+ continuation.resume(freshLocation?.toLocationPoint())
+ }
+ }
+ }
+ .addOnFailureListener {
+ continuation.resume(null)
+ }
+ } catch (e: SecurityException) {
+ continuation.resume(null)
+ }
+ }
+ }
+
+ fun locationUpdates(): Flow = callbackFlow {
+ if (!hasPermissions()) {
+ close(LocationError.PermissionsDenied)
+ return@callbackFlow
+ }
+
+ val locationManager = ctx.getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager
+ if (!locationManager.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER)) {
+ close(LocationError.GPSDisabled)
+ return@callbackFlow
+ }
+
+ val locationRequest = LocationRequest.Builder(
+ Priority.PRIORITY_HIGH_ACCURACY,
+ 5000L // 5 sec
+ ).apply {
+ setMinUpdateIntervalMillis(2000L)
+ }.build()
+
+ val locationCallback = object : LocationCallback() {
+ override fun onLocationResult(locationResult: LocationResult) {
+ locationResult.lastLocation?.let { location ->
+ trySend(location.toLocationPoint())
+ }
+ }
+ }
+
+ try {
+ fusedLocationClient.requestLocationUpdates(
+ locationRequest,
+ locationCallback,
+ ctx.mainLooper
+ )
+ } catch (e: SecurityException) {
+ close(LocationError.PermissionsDenied)
+ return@callbackFlow
+ }
+
+ awaitClose {
+ fusedLocationClient.removeLocationUpdates(locationCallback)
+ }
+ }
+
+ private fun requestFreshLocation(callback: (Location?) -> Unit) {
+ val locationRequest = LocationRequest.Builder(
+ Priority.PRIORITY_HIGH_ACCURACY,
+ 1000L
+ ).setMaxUpdates(1).build()
+
+ val locationCallback = object : LocationCallback() {
+ override fun onLocationResult(locationResult: LocationResult) {
+ callback(locationResult.lastLocation)
+ }
+ }
+
+ try {
+ fusedLocationClient.requestLocationUpdates(
+ locationRequest,
+ locationCallback,
+ Looper.getMainLooper()
+ )
+ } catch (e: SecurityException) {
+ callback(null)
+ }
+ }
+
+ private fun Location.toLocationPoint(): LocationPoint {
+ return LocationPoint(
+ latitude = latitude,
+ longitude = longitude,
+ timestamp = time
+ )
+ }
+
+}
diff --git a/app/src/main/java/com/example/goodroad/features/network/api/GoodRoadApi.kt b/app/src/main/java/com/example/goodroad/features/network/api/GoodRoadApi.kt
new file mode 100644
index 0000000..60d904d
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/features/network/api/GoodRoadApi.kt
@@ -0,0 +1,17 @@
+package com.example.goodroad.features.network.api
+
+import com.example.goodroad.model.RouteRequest
+import com.example.goodroad.model.RouteResponse
+import retrofit2.http.Body
+import retrofit2.http.POST
+import retrofit2.http.Query
+import retrofit2.http.GET
+import retrofit2.http.PUT
+import retrofit2.http.Header
+import com.example.goodroad.obstacle.ObstacleMapItemResp
+import com.example.goodroad.obstacle.PolicyItem
+import com.example.goodroad.obstacle.ReplacePolicyReq
+interface GoodRoadApi {
+ @POST("/api/v1/routes")
+ suspend fun getRoute(@Body request: RouteRequest): RouteResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/features/network/utils/decodePoints.kt b/app/src/main/java/com/example/goodroad/features/network/utils/decodePoints.kt
new file mode 100644
index 0000000..f42fbed
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/features/network/utils/decodePoints.kt
@@ -0,0 +1,14 @@
+package com.example.goodroad.features.network.utils
+
+import com.google.maps.android.PolyUtil
+import com.example.goodroad.domain.model.LocationPoint
+
+fun decodePoints(encodedPoints: String): List {
+ return PolyUtil.decode(encodedPoints).map {
+ LocationPoint(
+ latitude = it.latitude,
+ longitude = it.longitude
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/model/RouteRequest.kt b/app/src/main/java/com/example/goodroad/model/RouteRequest.kt
new file mode 100644
index 0000000..03f9165
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/model/RouteRequest.kt
@@ -0,0 +1,36 @@
+package com.example.goodroad.model
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class RouteRequest(
+ val start: String, // (lat,lon)
+ val end: String, // (lat,lon)
+ @JsonProperty("user_id")
+ val userId: String? = null,
+
+ @JsonProperty("max_stairs")
+ val maxStairsCount: Int? = null, // сколько ступенек максимум
+ @JsonProperty("max_slope")
+ val maxSlopeAngle: Double? = null, // макс угол уклона
+ @JsonProperty("max_curb_height")
+ val maxCurbHeight: Int? = null, // макс высота бордюра
+ @JsonProperty("min_path_width")
+ val minPathWidth: Int? = null, // мин ширина прохода
+
+ @JsonProperty("avoid_stairs")
+ val avoidStairs: Boolean = false, // избегать лестниц
+ @JsonProperty("need_ramp")
+ val needRamp: Boolean = false, // нужен пандус
+ @JsonProperty("avoid_bad_road")
+ val avoidBadRoad: Boolean = false, // избегать плохих дорог
+
+ // какие поверхности избегать
+ @JsonProperty("avoid_surfaces")
+ val avoidSurfaceTypes: List = emptyList(), // "SAND", "GRAVEL"
+
+ val locale: String = "ru", // язык инструкций
+ @JsonProperty("alternatives")
+ val needAlternatives: Boolean = true, // нужны ли альтернативы
+ @JsonProperty("points_encoded")
+ val pointsEncoded: Boolean = true
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/model/RouteResponse.kt b/app/src/main/java/com/example/goodroad/model/RouteResponse.kt
new file mode 100644
index 0000000..a083e40
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/model/RouteResponse.kt
@@ -0,0 +1,60 @@
+package com.example.goodroad.model
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.time.Duration
+
+data class RouteResponse(
+ val id: String, // уникальный ID маршрута
+ val paths: List, // варианты маршрутов
+ val info: ResponseInfo? = null
+)
+
+data class PathResponse(
+ val distance: Double,
+ val time: Long,
+
+ @JsonProperty("points_encoded")
+ val pointsEncoded: Boolean = true,
+ val points: String,
+
+ val obstacles: List = emptyList(),
+
+ @JsonProperty("route_type")
+ val routeType: String = "fast", // fast, safe, balanced
+)
+
+
+data class ObstacleResponse(
+ val id: String,
+
+ @JsonProperty("lat")
+ val latitude: Double,
+
+ @JsonProperty("lon")
+ val longitude: Double,
+
+ val type: String, // STAIRS, CURB и т.д.
+
+ val details: ObstacleDetailsResponse? = null
+)
+
+data class ObstacleDetailsResponse(
+ @JsonProperty("step_count")
+ val stepCount: Int? = null, // для лестниц
+
+ @JsonProperty("height_cm")
+ val heightCm: Int? = null, // для бордюров
+
+ @JsonProperty("angle_degrees")
+ val angleDegrees: Double? = null, // для уклонов
+
+ @JsonProperty("has_ramp")
+ val hasRamp: Boolean? = null, // есть пандус
+
+ @JsonProperty("surface_type")
+ val surfaceType: String? = null // покрытие
+)
+
+data class ResponseInfo(
+ val took: Double
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/obstacle/ObstacleModels.kt b/app/src/main/java/com/example/goodroad/obstacle/ObstacleModels.kt
new file mode 100644
index 0000000..3d2a594
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/obstacle/ObstacleModels.kt
@@ -0,0 +1,26 @@
+package com.example.goodroad.obstacle
+
+// data/obstacles/ObstacleModels.kt
+
+import java.time.Instant
+
+data class ObstacleMapItemResp(
+ val id: String,
+ val type: String,
+ val latitude: Double,
+ val longitude: Double,
+ val address: AddressResp?,
+ val severityEstimate: Short?,
+ val reviewsCount: Int,
+ val lastReviewedAt: Instant?
+)
+
+data class AddressResp(
+ val country: String?,
+ val region: String?,
+ val localityType: String?,
+ val city: String?,
+ val street: String?,
+ val house: String?,
+ val placeName: String?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/obstacle/UserObstacleModels.kt b/app/src/main/java/com/example/goodroad/obstacle/UserObstacleModels.kt
new file mode 100644
index 0000000..9fee99a
--- /dev/null
+++ b/app/src/main/java/com/example/goodroad/obstacle/UserObstacleModels.kt
@@ -0,0 +1,11 @@
+package com.example.goodroad.obstacle
+
+data class PolicyItem(
+ val obstacleType: String,
+ val selected: Boolean,
+ val maxAllowedSeverity: Short?
+)
+
+data class ReplacePolicyReq(
+ val items: List
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/goodroad/ui/user/UserProfileScreen.kt b/app/src/main/java/com/example/goodroad/ui/user/UserProfileScreen.kt
index abe96de..3b42834 100644
--- a/app/src/main/java/com/example/goodroad/ui/user/UserProfileScreen.kt
+++ b/app/src/main/java/com/example/goodroad/ui/user/UserProfileScreen.kt
@@ -12,6 +12,9 @@ import androidx.compose.ui.unit.sp
import com.example.goodroad.ui.auth.AuthButton
import com.example.goodroad.ui.theme.*
import com.example.goodroad.ui.viewmodel.UserViewModel
+import android.content.Intent
+import com.example.goodroad.MapActivity
+import androidx.compose.ui.platform.LocalContext
@Composable
fun UserProfileScreen(
@@ -99,11 +102,15 @@ fun UserProfileScreen(
Spacer(modifier = Modifier.height(10.dp))
+ val context = LocalContext.current
+
AuthButton(
text = "Перейти на карту",
backgroundColor = UrbanBrown,
contentColor = WhiteSoft
) {
+ val intent = Intent(context, MapActivity::class.java)
+ context.startActivity(intent)
}
Spacer(Modifier.height(10.dp))
diff --git a/app/src/main/res/drawable/rounded_button.xml b/app/src/main/res/drawable/rounded_button.xml
new file mode 100644
index 0000000..7882a40
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_button.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/rounded_edittext.xml b/app/src/main/res/drawable/rounded_edittext.xml
new file mode 100644
index 0000000..418a8f2
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_edittext.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml
new file mode 100644
index 0000000..963334d
--- /dev/null
+++ b/app/src/main/res/layout/activity_map.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..6cd15ee
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,29 @@
+
+
+ #4F87C9
+ #6FAE8A
+ #8B7AC6
+ #D56B63
+
+ #A28A72
+
+ #F7F5F0
+ #EEE7DD
+ #D8CEC0
+
+ #F3EFE7
+ #DCCFBE
+ #BFDCF3
+ #CFE3C8
+
+ #4E9B6F
+ #4F87C9
+ #7C6BCB
+ #D56B63
+
+ #2F2B28
+ #7A6F66
+ #FFFBF7
+
+ #9E9E9E
+
\ No newline at end of file