Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
9 changes: 7 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
android:supportsRtl="true"
android:theme="@style/Theme.GoodRoad"
android:usesCleartextTraffic="true">

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".MapActivity"
android:exported="false" />

</application>

</manifest>
</manifest>
286 changes: 286 additions & 0 deletions app/src/main/java/com/example/goodroad/MapActivity.kt
Original file line number Diff line number Diff line change
@@ -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<out String>,
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
Loading