From 9d685a4c87780ea4c59230ef4c90eb04b528ad9c Mon Sep 17 00:00:00 2001 From: mitulsheth Date: Fri, 17 Apr 2026 13:28:50 +0530 Subject: [PATCH] fix(bug): Route deviation not triggered in some cases --- cardinal-android/app/build.gradle.kts | 1 + .../CardinalFerrostarWrapperFactory.kt | 60 +++++ .../navigation/CustomDeviationDetector.kt | 66 +++++ .../FerrostarRouteDeviationDetector.kt | 58 +++++ .../navigation/FerrostarWrapperFactory.kt | 30 +++ .../navigation/PolylineDistanceCalculator.kt | 157 ++++++++++++ .../cardinal/di/FerrostarWrapperModule.kt | 36 +++ .../earth/maps/cardinal/di/NetworkModule.kt | 55 +++++ .../maps/cardinal/domain/DeviationDetector.kt | 28 +++ .../maps/cardinal/routing/FerrostarWrapper.kt | 27 ++- .../routing/FerrostarWrapperRepository.kt | 78 ++---- .../maps/cardinal/routing/RoutingOptions.kt | 6 +- .../routing/ValhallaRoutingService.kt | 20 +- .../navigation/CustomDeviationDetectorTest.kt | 174 ++++++++++++++ .../FerrostarRouteDeviationDetectorTest.kt | 227 ++++++++++++++++++ .../PolylineDistanceCalculatorTest.kt | 169 +++++++++++++ cardinal-android/gradle/libs.versions.toml | 2 + 17 files changed, 1126 insertions(+), 68 deletions(-) create mode 100644 cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/CardinalFerrostarWrapperFactory.kt create mode 100644 cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/CustomDeviationDetector.kt create mode 100644 cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/FerrostarRouteDeviationDetector.kt create mode 100644 cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/FerrostarWrapperFactory.kt create mode 100644 cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/PolylineDistanceCalculator.kt create mode 100644 cardinal-android/app/src/main/java/earth/maps/cardinal/di/FerrostarWrapperModule.kt create mode 100644 cardinal-android/app/src/main/java/earth/maps/cardinal/di/NetworkModule.kt create mode 100644 cardinal-android/app/src/main/java/earth/maps/cardinal/domain/DeviationDetector.kt create mode 100644 cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/CustomDeviationDetectorTest.kt create mode 100644 cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/FerrostarRouteDeviationDetectorTest.kt create mode 100644 cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/PolylineDistanceCalculatorTest.kt diff --git a/cardinal-android/app/build.gradle.kts b/cardinal-android/app/build.gradle.kts index 141cbb4..70561d7 100644 --- a/cardinal-android/app/build.gradle.kts +++ b/cardinal-android/app/build.gradle.kts @@ -216,6 +216,7 @@ dependencies { implementation(libs.ferrostar.maplibreui) implementation(libs.ferrostar.composeui) implementation(libs.okhttp3) + implementation(libs.logging.interceptor) implementation(libs.androidaddressformatter) implementation(libs.eos.telemetry) diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/CardinalFerrostarWrapperFactory.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/CardinalFerrostarWrapperFactory.kt new file mode 100644 index 0000000..1d45533 --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/CardinalFerrostarWrapperFactory.kt @@ -0,0 +1,60 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.data.navigation + +import android.content.Context +import com.stadiamaps.ferrostar.core.SpokenInstructionObserver +import dagger.hilt.android.qualifiers.ApplicationContext +import earth.maps.cardinal.data.LocationRepository +import earth.maps.cardinal.data.OrientationRepository +import earth.maps.cardinal.data.RoutingMode +import earth.maps.cardinal.data.room.RoutingProfileRepository +import earth.maps.cardinal.routing.FerrostarWrapper +import earth.maps.cardinal.routing.RoutingOptions +import okhttp3.OkHttpClient +import javax.inject.Inject + +class CardinalFerrostarWrapperFactory @Inject constructor( + @ApplicationContext private val context: Context, + private val locationRepository: LocationRepository, + private val orientationRepository: OrientationRepository, + private val androidTtsObserver: SpokenInstructionObserver, + private val routingProfileRepository: RoutingProfileRepository, + private val okHttpClient: OkHttpClient +) : FerrostarWrapperFactory { + + override fun create( + mode: RoutingMode, + endpoint: String, + routingOptions: RoutingOptions? + ): FerrostarWrapper { + + return FerrostarWrapper( + context = context, + locationRepository = locationRepository, + orientationRepository = orientationRepository, + mode = mode, + localValhallaEndpoint = endpoint, + androidTtsObserver = androidTtsObserver, + routingProfileRepository = routingProfileRepository, + routingOptions = routingOptions, + okHttpClient = okHttpClient + ) + } +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/CustomDeviationDetector.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/CustomDeviationDetector.kt new file mode 100644 index 0000000..1db3649 --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/CustomDeviationDetector.kt @@ -0,0 +1,66 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package earth.maps.cardinal.data.navigation + +import earth.maps.cardinal.data.LatLng +import earth.maps.cardinal.domain.DeviationDetector + +class CustomDeviationDetector( + private val distanceCalculator: PolylineDistanceCalculator +) : DeviationDetector { + + private var lastRouteHash: Int = 0 + private var lastOffRouteTs: Long = 0L + + override fun isOffRoute( + location: LatLng, + route: List + ): Boolean { + + if (route.size < MIN_ROUTE_POINTS) return false + + val currentHash = route.hashCode() + if (currentHash != lastRouteHash) { + distanceCalculator.resetCache() + lastRouteHash = currentHash + } + + val distance = distanceCalculator.distanceFromPolyline(location, route) + + val now = System.currentTimeMillis() + + val isOffRoute = if (distance > OFF_ROUTE_THRESHOLD_METERS) { + if (now - lastOffRouteTs > OFF_ROUTE_DEBOUNCE_MS) { + lastOffRouteTs = now + true + } else { + false + } + } else { + false + } + + return isOffRoute + } + + companion object { + private const val OFF_ROUTE_THRESHOLD_METERS = 15.0 + private const val OFF_ROUTE_DEBOUNCE_MS = 1500L + private const val MIN_ROUTE_POINTS = 2 + } +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/FerrostarRouteDeviationDetector.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/FerrostarRouteDeviationDetector.kt new file mode 100644 index 0000000..3b8d957 --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/FerrostarRouteDeviationDetector.kt @@ -0,0 +1,58 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.data.navigation + +import earth.maps.cardinal.data.LatLng +import earth.maps.cardinal.domain.DeviationDetector +import uniffi.ferrostar.Route +import uniffi.ferrostar.RouteDeviation +import uniffi.ferrostar.RouteDeviationDetector +import uniffi.ferrostar.RouteStep +import uniffi.ferrostar.UserLocation + +class FerrostarRouteDeviationDetector( + private val deviationDetector: DeviationDetector +) : RouteDeviationDetector { + + override fun checkRouteDeviation( + location: UserLocation, + route: Route, + currentRouteStep: RouteStep + ): RouteDeviation { + + val raw = LatLng( + location.coordinates.lat, + location.coordinates.lng + ) + + val geometry = route.geometry.map { + LatLng(it.lat, it.lng) + } + + return if (deviationDetector.isOffRoute(raw, geometry)) { + RouteDeviation.OffRoute(DEVIATION_FROM_ROUTE_LINE) + } else { + RouteDeviation.NoDeviation + } + } + + companion object { + private const val DEVIATION_FROM_ROUTE_LINE = 30.0 + } +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/FerrostarWrapperFactory.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/FerrostarWrapperFactory.kt new file mode 100644 index 0000000..6b04e5f --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/FerrostarWrapperFactory.kt @@ -0,0 +1,30 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package earth.maps.cardinal.data.navigation + +import earth.maps.cardinal.data.RoutingMode +import earth.maps.cardinal.routing.FerrostarWrapper +import earth.maps.cardinal.routing.RoutingOptions + +interface FerrostarWrapperFactory { + fun create( + mode: RoutingMode, + endpoint: String, + routingOptions: RoutingOptions? = null + ): FerrostarWrapper +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/PolylineDistanceCalculator.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/PolylineDistanceCalculator.kt new file mode 100644 index 0000000..2c8599a --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/navigation/PolylineDistanceCalculator.kt @@ -0,0 +1,157 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.data.navigation + +import earth.maps.cardinal.data.LatLng +import javax.inject.Inject +import kotlin.math.cos +import kotlin.math.hypot + +class PolylineDistanceCalculator @Inject constructor() { + + private var lastSegmentIndex = 0 + + fun distanceFromPolyline( + point: LatLng, + polyline: List + ): Double { + if (polyline.size < MIN_POLYLINE_POINTS) return Double.MAX_VALUE + + val previousIndex = lastSegmentIndex + + var minDistance = Double.MAX_VALUE + var bestIndex = previousIndex + + val start = maxOf(0, previousIndex - SEARCH_WINDOW) + val end = minOf(polyline.size - 2, previousIndex + SEARCH_WINDOW) + + // 1) Windowed search (fast path) + for (i in start..end) { + val d = distanceToSegmentMeters(point, polyline[i], polyline[i + 1]) + + if (d < minDistance) { + minDistance = d + bestIndex = i + + if (minDistance < EARLY_EXIT_DISTANCE_METERS) { + lastSegmentIndex = bestIndex + return minDistance + } + } + } + + // 2) Fallback conditions + + // A) Edge detection → likely missed correct segment + val isNearWindowEdge = + bestIndex <= start + EDGE_MARGIN || + bestIndex >= end - EDGE_MARGIN + + // B) Movement jump → GPS jumped far from previous segment + val jumped = kotlin.math.abs(bestIndex - previousIndex) > SEARCH_WINDOW + + // C) Distance too large → obvious mismatch + val isDistanceLarge = minDistance > FALLBACK_TRIGGER_DISTANCE_METERS + + val shouldFallback = jumped || isNearWindowEdge || isDistanceLarge + + // 3) Full scan fallback (rare but critical) + if (shouldFallback) { + var globalMin = minDistance + var globalBestIndex = bestIndex + + for (i in 0 until polyline.size - 1) { + val d = distanceToSegmentMeters(point, polyline[i], polyline[i + 1]) + if (d < globalMin) { + globalMin = d + globalBestIndex = i + } + } + + minDistance = globalMin + bestIndex = globalBestIndex + } + + lastSegmentIndex = bestIndex + return minDistance + } + + /* + * Accurate distance using local projection (meters) + */ + private fun distanceToSegmentMeters(p: LatLng, a: LatLng, b: LatLng): Double { + + val latRad = Math.toRadians(p.latitude) + + val ax = projectX(a.longitude, latRad) + val ay = projectY(a.latitude) + + val bx = projectX(b.longitude, latRad) + val by = projectY(b.latitude) + + val px = projectX(p.longitude, latRad) + val py = projectY(p.latitude) + + val dx = bx - ax + val dy = by - ay + + if (dx == ZERO && dy == ZERO) { + return hypot(px - ax, py - ay) + } + + val t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy) + val clamped = t.coerceIn(ZERO, ONE) + + val projX = ax + clamped * dx + val projY = ay + clamped * dy + + return hypot(px - projX, py - projY) + } + + /* + * Longitude → meters (scaled by latitude) + */ + private fun projectX(lon: Double, latRad: Double): Double { + return lon * cos(latRad) * METERS_PER_DEGREE + } + + /* + * Latitude → meters + */ + private fun projectY(lat: Double): Double { + return lat * METERS_PER_DEGREE + } + + fun resetCache() { + lastSegmentIndex = 0 + } + + companion object { + private const val FALLBACK_TRIGGER_DISTANCE_METERS = 50.0 + private const val EDGE_MARGIN = 2 + private const val METERS_PER_DEGREE = 111_320.0 + private const val MIN_POLYLINE_POINTS = 2 + + private const val SEARCH_WINDOW = 10 + private const val EARLY_EXIT_DISTANCE_METERS = 5.0 + + private const val ZERO = 0.0 + private const val ONE = 1.0 + } +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/di/FerrostarWrapperModule.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/di/FerrostarWrapperModule.kt new file mode 100644 index 0000000..e0fef8e --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/di/FerrostarWrapperModule.kt @@ -0,0 +1,36 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package earth.maps.cardinal.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import earth.maps.cardinal.data.navigation.CardinalFerrostarWrapperFactory +import earth.maps.cardinal.data.navigation.FerrostarWrapperFactory + +@Module +@InstallIn(SingletonComponent::class) +abstract class FerrostarWrapperModule { + + @Binds + abstract fun bindFerrostarWrapperFactory( + cardinalFerrostarWrapperFactory: CardinalFerrostarWrapperFactory + ): FerrostarWrapperFactory +} + diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/di/NetworkModule.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/di/NetworkModule.kt new file mode 100644 index 0000000..638ca41 --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/di/NetworkModule.kt @@ -0,0 +1,55 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package earth.maps.cardinal.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import earth.maps.cardinal.BuildConfig +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + val builder = OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT_SEC, TimeUnit.SECONDS) + .writeTimeout(WRITE_TIMEOUT_SEC, TimeUnit.SECONDS) + .callTimeout(CALL_TIMEOUT_SEC, TimeUnit.SECONDS) + if (BuildConfig.DEBUG) { + val logging = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + } + builder.addInterceptor(logging) + } + return builder.build() + } + + private const val CONNECT_TIMEOUT_SEC = 10L + private const val READ_TIMEOUT_SEC = 30L + private const val WRITE_TIMEOUT_SEC = 30L + private const val CALL_TIMEOUT_SEC = 30L +} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/domain/DeviationDetector.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/domain/DeviationDetector.kt new file mode 100644 index 0000000..3ee5119 --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/domain/DeviationDetector.kt @@ -0,0 +1,28 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package earth.maps.cardinal.domain + +import earth.maps.cardinal.data.LatLng + +interface DeviationDetector { + fun isOffRoute( + location: LatLng, + route: List + ): Boolean +} + diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapper.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapper.kt index ae1b001..d3a4f72 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapper.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapper.kt @@ -31,6 +31,9 @@ import com.stadiamaps.ferrostar.core.toUserLocation import earth.maps.cardinal.data.LocationRepository import earth.maps.cardinal.data.OrientationRepository import earth.maps.cardinal.data.RoutingMode +import earth.maps.cardinal.data.navigation.CustomDeviationDetector +import earth.maps.cardinal.data.navigation.FerrostarRouteDeviationDetector +import earth.maps.cardinal.data.navigation.PolylineDistanceCalculator import earth.maps.cardinal.data.room.RoutingProfileRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -54,14 +57,14 @@ import kotlin.time.toJavaInstant class FerrostarWrapper( context: Context, private val locationRepository: LocationRepository, - private val orientationRepository: OrientationRepository, + val orientationRepository: OrientationRepository, private val mode: RoutingMode, private val localValhallaEndpoint: String, private val androidTtsObserver: SpokenInstructionObserver, routingProfileRepository: RoutingProfileRepository, - routingOptions: RoutingOptions? = null + routingOptions: RoutingOptions? = null, + private val okHttpClient: OkHttpClient ) { - private val coroutineScope = CoroutineScope(Dispatchers.Main) private val foregroundServiceManager: ForegroundServiceManager = @@ -119,13 +122,15 @@ class FerrostarWrapper( ?: routingProfileRepository.createDefaultOptionsForMode(mode) ?.toValhallaOptionsJson() )), - httpClient = OkHttpClientProvider(OkHttpClient()), + httpClient = OkHttpClientProvider(okHttpClient), locationProvider = locationProvider, navigationControllerConfig = NavigationControllerConfig( waypointAdvance = WaypointAdvanceMode.WaypointWithinRange(WAYPOINT_ADVANCE_RANGE), stepAdvanceCondition = stepAdvanceDistanceEntryAndExit(STEP_ADVANCE_DISTANCE_TO_END.toUShort(), STEP_ADVANCE_DISTANCE_AFTER_END.toUShort(), STEP_ADVANCE_MINIMUM_HORIZONTAL_ACCURACY.toUShort()), arrivalStepAdvanceCondition = stepAdvanceDistanceToEndOfStep(ARRIVAL_STEP_ADVANCE_DISTANCE.toUShort(), ARRIVAL_STEP_ADVANCE_MINIMUM_HORIZONTAL_ACCURACY.toUShort()), - routeDeviationTracking = RouteDeviationTracking.StaticThreshold(ROUTE_DEVIATION_MINIMUM_ACCURACY.toUShort(), ROUTE_DEVIATION_MAX_DEVIATION), + routeDeviationTracking = RouteDeviationTracking.Custom( + detector = getDeviationDetector() + ), snappedLocationCourseFiltering = COURSE_FILTERING ), foregroundServiceManager = foregroundServiceManager, @@ -150,13 +155,15 @@ class FerrostarWrapper( profile = mode.value, optionsJson = previousRouteOptions?.toValhallaOptionsJson() )), - httpClient = OkHttpClientProvider(OkHttpClient()), + httpClient = OkHttpClientProvider(okHttpClient), locationProvider = locationProvider, navigationControllerConfig = NavigationControllerConfig( waypointAdvance = WaypointAdvanceMode.WaypointWithinRange(WAYPOINT_ADVANCE_RANGE), stepAdvanceCondition = stepAdvanceDistanceEntryAndExit(STEP_ADVANCE_DISTANCE_TO_END.toUShort(), STEP_ADVANCE_DISTANCE_AFTER_END.toUShort(), STEP_ADVANCE_MINIMUM_HORIZONTAL_ACCURACY.toUShort()), arrivalStepAdvanceCondition = stepAdvanceDistanceToEndOfStep(ARRIVAL_STEP_ADVANCE_DISTANCE.toUShort(), ARRIVAL_STEP_ADVANCE_MINIMUM_HORIZONTAL_ACCURACY.toUShort()), - routeDeviationTracking = RouteDeviationTracking.StaticThreshold(ROUTE_DEVIATION_MINIMUM_ACCURACY.toUShort(), ROUTE_DEVIATION_MAX_DEVIATION), + routeDeviationTracking = RouteDeviationTracking.Custom( + detector = getDeviationDetector() + ), snappedLocationCourseFiltering = COURSE_FILTERING ), foregroundServiceManager = foregroundServiceManager @@ -164,6 +171,12 @@ class FerrostarWrapper( core.spokenInstructionObserver = androidTtsObserver } + private fun getDeviationDetector(): FerrostarRouteDeviationDetector { + val distanceCalculator = PolylineDistanceCalculator() + val deviationDetector = CustomDeviationDetector(distanceCalculator) + return FerrostarRouteDeviationDetector(deviationDetector) + } + companion object { const val WAYPOINT_ADVANCE_RANGE = 100.0 const val STEP_ADVANCE_DISTANCE_TO_END = 30u diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapperRepository.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapperRepository.kt index d1c4c49..a553b77 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapperRepository.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapperRepository.kt @@ -18,12 +18,9 @@ package earth.maps.cardinal.routing -import android.content.Context import com.stadiamaps.ferrostar.core.SpokenInstructionObserver -import dagger.hilt.android.qualifiers.ApplicationContext -import earth.maps.cardinal.data.LocationRepository -import earth.maps.cardinal.data.OrientationRepository import earth.maps.cardinal.data.RoutingMode +import earth.maps.cardinal.data.navigation.FerrostarWrapperFactory import earth.maps.cardinal.data.room.RoutingProfileRepository import earth.maps.cardinal.data.tts.MapsTtsObserver import kotlinx.coroutines.flow.MutableStateFlow @@ -35,12 +32,9 @@ import javax.inject.Singleton @Singleton class FerrostarWrapperRepository @Inject constructor( - @param:ApplicationContext - private val context: Context, - private val locationRepository: LocationRepository, - private val orientationRepository: OrientationRepository, private val routingProfileRepository: RoutingProfileRepository, - private val androidTtsObserver: SpokenInstructionObserver + private val androidTtsObserver: SpokenInstructionObserver, + private val factory: FerrostarWrapperFactory ) { private val _isInitialized = MutableStateFlow(false) val isInitialized = _isInitialized.asStateFlow() @@ -80,59 +74,29 @@ class FerrostarWrapperRepository @Inject constructor( } fun setValhallaEndpoint(endpoint: String) { - _walking = FerrostarWrapper( - context, - locationRepository, - orientationRepository, - RoutingMode.PEDESTRIAN, - endpoint, - androidTtsObserver, - routingProfileRepository + _walking = factory.create( + mode = RoutingMode.PEDESTRIAN, + endpoint = endpoint ) - _cycling = FerrostarWrapper( - context, - locationRepository, - orientationRepository, - RoutingMode.BICYCLE, - endpoint, - androidTtsObserver, - routingProfileRepository + _cycling = factory.create( + mode = RoutingMode.BICYCLE, + endpoint = endpoint ) - _driving = FerrostarWrapper( - context, - locationRepository, - orientationRepository, - RoutingMode.AUTO, - endpoint, - androidTtsObserver, - routingProfileRepository + _driving = factory.create( + mode = RoutingMode.AUTO, + endpoint = endpoint ) - _truck = FerrostarWrapper( - context, - locationRepository, - orientationRepository, - RoutingMode.TRUCK, - endpoint, - androidTtsObserver, - routingProfileRepository + _truck = factory.create( + mode = RoutingMode.TRUCK, + endpoint = endpoint ) - _motorScooter = FerrostarWrapper( - context, - locationRepository, - orientationRepository, - RoutingMode.MOTOR_SCOOTER, - endpoint, - androidTtsObserver, - routingProfileRepository + _motorScooter = factory.create( + mode = RoutingMode.MOTOR_SCOOTER, + endpoint = endpoint ) - _motorcycle = FerrostarWrapper( - context, - locationRepository, - orientationRepository, - RoutingMode.MOTORCYCLE, - endpoint, - androidTtsObserver, - routingProfileRepository + _motorcycle = factory.create( + mode = RoutingMode.MOTORCYCLE, + endpoint = endpoint ) _isInitialized.value = true diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/RoutingOptions.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/RoutingOptions.kt index 0ac67a6..034c203 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/RoutingOptions.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/RoutingOptions.kt @@ -70,9 +70,9 @@ data class AutoRoutingOptions( override val costingType: String = COSTING_TYPE_AUTO, // Maneuver and access penalties - override val maneuverPenalty: Double? = DEFAULT_MANEUVER_PENALTY, - override val gateCost: Double? = DEFAULT_GATE_COST, - override val tollBoothCost: Double? = DEFAULT_TOLL_BOOTH_COST, + override val maneuverPenalty: Double? = null, + override val gateCost: Double? = null, + override val tollBoothCost: Double? = null, override val privateAccessPenalty: Double? = null, // Road type preferences (0-1 range) diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/ValhallaRoutingService.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/ValhallaRoutingService.kt index 55cd65b..f464f78 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/ValhallaRoutingService.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/routing/ValhallaRoutingService.kt @@ -19,11 +19,15 @@ package earth.maps.cardinal.routing import android.util.Log +import earth.maps.cardinal.BuildConfig import earth.maps.cardinal.data.AppPreferenceRepository import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.android.Android +import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.post import io.ktor.client.request.setBody @@ -44,7 +48,21 @@ class ValhallaRoutingService(private val appPreferenceRepository: AppPreferenceR isLenient = true }) } - install(Logging) + install(HttpTimeout) { + requestTimeoutMillis = 30_000 + connectTimeoutMillis = 10_000 + socketTimeoutMillis = 30_000 + } + if (BuildConfig.DEBUG) { + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + Log.d("KtorClient", message) + } + } + level = LogLevel.ALL + } + } } override suspend fun getRoute( diff --git a/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/CustomDeviationDetectorTest.kt b/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/CustomDeviationDetectorTest.kt new file mode 100644 index 0000000..af72c88 --- /dev/null +++ b/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/CustomDeviationDetectorTest.kt @@ -0,0 +1,174 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package earth.maps.cardinal.data.navigation + +import earth.maps.cardinal.data.LatLng +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class CustomDeviationDetectorTest { + + private val distanceCalculator = mockk(relaxed = true) + + private lateinit var detector: CustomDeviationDetector + + @Before + fun setup() { + detector = CustomDeviationDetector(distanceCalculator) + } + + private fun latLng(lat: Double, lon: Double) = LatLng(lat, lon) + + private val route = listOf( + latLng(0.0, 0.0), + latLng(0.0, 1.0) + ) + + @Test + fun `should return false when route has less than 2 points`() { + val result = detector.isOffRoute( + location = latLng(0.0, 0.0), + route = listOf(latLng(0.0, 0.0)) + ) + + assertFalse(result) + verify(exactly = 0) { distanceCalculator.distanceFromPolyline(any(), any()) } + } + + @Test + fun `should return false when distance is within threshold`() { + every { + distanceCalculator.distanceFromPolyline(any(), any()) + } returns 10.0 // below 15 + + val result = detector.isOffRoute(latLng(0.0, 0.5), route) + + assertFalse(result) + } + + @Test + fun `should return true when distance exceeds threshold`() { + every { + distanceCalculator.distanceFromPolyline(any(), any()) + } returns 20.0 + + val result = detector.isOffRoute(latLng(1.0, 1.0), route) + + assertTrue(result) + } + + @Test + fun `should debounce repeated off route triggers`() { + every { + distanceCalculator.distanceFromPolyline(any(), any()) + } returns 20.0 + + val location = latLng(1.0, 1.0) + + val first = detector.isOffRoute(location, route) + val second = detector.isOffRoute(location, route) + + assertTrue(first) + assertFalse(second) // debounced + } + + @Test + fun `should allow off route after debounce period`() { + every { + distanceCalculator.distanceFromPolyline(any(), any()) + } returns 20.0 + + val location = latLng(1.0, 1.0) + + val first = detector.isOffRoute(location, route) + + Thread.sleep(1600) // > 1500ms debounce + + val second = detector.isOffRoute(location, route) + + assertTrue(first) + assertTrue(second) + } + + @Test + fun `should reset cache when route changes`() { + every { + distanceCalculator.distanceFromPolyline(any(), any()) + } returns 10.0 + + val route1 = listOf(latLng(0.0, 0.0), latLng(0.0, 1.0)) + val route2 = listOf(latLng(1.0, 1.0), latLng(2.0, 2.0)) + + detector.isOffRoute(latLng(0.0, 0.5), route1) + detector.isOffRoute(latLng(0.0, 0.5), route2) + + verify(atLeast = 1) { + distanceCalculator.resetCache() + } + } + + @Test + fun `should not reset cache for same route`() { + every { + distanceCalculator.distanceFromPolyline(any(), any()) + } returns 10.0 + + detector.isOffRoute(latLng(0.0, 0.5), route) + detector.isOffRoute(latLng(0.0, 0.6), route) + + verify(exactly = 1) { + distanceCalculator.resetCache() + } + } + + @Test + fun `should return false when distance equals threshold`() { + every { + distanceCalculator.distanceFromPolyline(any(), any()) + } returns 15.0 + + val result = detector.isOffRoute(latLng(0.0, 0.5), route) + + assertFalse(result) + } + + @Test + fun `should handle fluctuating distances correctly`() { + every { + distanceCalculator.distanceFromPolyline(any(), any()) + } returnsMany listOf(10.0, 20.0, 10.0, 20.0) + + val location = latLng(0.0, 0.5) + + val r1 = detector.isOffRoute(location, route) // false + val r2 = detector.isOffRoute(location, route) // true + val r3 = detector.isOffRoute(location, route) // false + val r4 = detector.isOffRoute(location, route) // debounced + + assertFalse(r1) + assertTrue(r2) + assertFalse(r3) + assertFalse(r4) + } + +} diff --git a/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/FerrostarRouteDeviationDetectorTest.kt b/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/FerrostarRouteDeviationDetectorTest.kt new file mode 100644 index 0000000..94635fc --- /dev/null +++ b/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/FerrostarRouteDeviationDetectorTest.kt @@ -0,0 +1,227 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.data.navigation + +import earth.maps.cardinal.data.LatLng +import earth.maps.cardinal.domain.DeviationDetector +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import uniffi.ferrostar.Route +import uniffi.ferrostar.RouteDeviation +import uniffi.ferrostar.UserLocation + +class FerrostarRouteDeviationDetectorTest { + + private val deviationDetector = mockk() + + private lateinit var detector: FerrostarRouteDeviationDetector + + @Before + fun setup() { + detector = FerrostarRouteDeviationDetector(deviationDetector) + } + + private fun latLng(lat: Double, lng: Double) = LatLng(lat, lng) + + @Test + fun `should return NoDeviation when deviationDetector returns false`() { + every { + deviationDetector.isOffRoute(any(), any()) + } returns false + + val location = mockk { + every { coordinates.lat } returns 12.0 + every { coordinates.lng } returns 77.0 + } + + val route = mockk { + every { geometry } returns listOf( + mockk { + every { lat } returns 12.0 + every { lng } returns 77.0 + } + ) + } + + val result = detector.checkRouteDeviation(location, route, mockk()) + + assertTrue(result is RouteDeviation.NoDeviation) + } + + @Test + fun `should return OffRoute when deviationDetector returns true`() { + every { + deviationDetector.isOffRoute(any(), any()) + } returns true + + val location = mockk { + every { coordinates.lat } returns 12.0 + every { coordinates.lng } returns 77.0 + } + + val route = mockk { + every { geometry } returns listOf( + mockk { + every { lat } returns 12.0 + every { lng } returns 77.0 + } + ) + } + + val result = detector.checkRouteDeviation(location, route, mockk()) + + assertTrue(result is RouteDeviation.OffRoute) + + val offRoute = result as RouteDeviation.OffRoute + assertEquals(30.0, offRoute.deviationFromRouteLine, 0.0) + } + + @Test + fun `should map UserLocation to LatLng correctly`() { + val slotLocation = slot() + + every { + deviationDetector.isOffRoute(capture(slotLocation), any()) + } returns false + + val location = mockk { + every { coordinates.lat } returns 10.5 + every { coordinates.lng } returns 20.5 + } + + val route = mockk { + every { geometry } returns emptyList() + } + + detector.checkRouteDeviation(location, route, mockk()) + + assertEquals(10.5, slotLocation.captured.latitude, 0.0) + assertEquals(20.5, slotLocation.captured.longitude, 0.0) + } + + @Test + fun `should map route geometry to LatLng list`() { + val slotRoute = slot>() + + every { + deviationDetector.isOffRoute(any(), capture(slotRoute)) + } returns false + + val route = mockk { + every { geometry } returns listOf( + mockk { + every { lat } returns 1.0 + every { lng } returns 2.0 + }, + mockk { + every { lat } returns 3.0 + every { lng } returns 4.0 + } + ) + } + + val location = mockk { + every { coordinates.lat } returns 0.0 + every { coordinates.lng } returns 0.0 + } + + detector.checkRouteDeviation(location, route, mockk()) + + val captured = slotRoute.captured + + assertEquals(2, captured.size) + assertEquals(1.0, captured[0].latitude, 0.0) + assertEquals(2.0, captured[0].longitude, 0.0) + assertEquals(3.0, captured[1].latitude, 0.0) + assertEquals(4.0, captured[1].longitude, 0.0) + } + + @Test + fun `should call deviationDetector once`() { + every { + deviationDetector.isOffRoute(any(), any()) + } returns false + + val location = mockk { + every { coordinates.lat } returns 0.0 + every { coordinates.lng } returns 0.0 + } + + val route = mockk { + every { geometry } returns emptyList() + } + + detector.checkRouteDeviation(location, route, mockk()) + + verify(exactly = 1) { + deviationDetector.isOffRoute(any(), any()) + } + } + + @Test + fun `should handle empty geometry`() { + every { + deviationDetector.isOffRoute(any(), any()) + } returns false + + val location = mockk { + every { coordinates.lat } returns 0.0 + every { coordinates.lng } returns 0.0 + } + + val route = mockk { + every { geometry } returns emptyList() + } + + val result = detector.checkRouteDeviation(location, route, mockk()) + + assertTrue(result is RouteDeviation.NoDeviation) + } + + @Test + fun `should handle multiple geometry points when off route`() { + every { + deviationDetector.isOffRoute(any(), any()) + } returns true + + val location = mockk { + every { coordinates.lat } returns 5.0 + every { coordinates.lng } returns 5.0 + } + + val route = mockk { + every { geometry } returns listOf( + mockk { every { lat } returns 1.0; every { lng } returns 1.0 }, + mockk { every { lat } returns 2.0; every { lng } returns 2.0 }, + mockk { every { lat } returns 3.0; every { lng } returns 3.0 } + ) + } + + val result = detector.checkRouteDeviation(location, route, mockk()) + + assertTrue(result is RouteDeviation.OffRoute) + } + +} diff --git a/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/PolylineDistanceCalculatorTest.kt b/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/PolylineDistanceCalculatorTest.kt new file mode 100644 index 0000000..ccbd088 --- /dev/null +++ b/cardinal-android/app/src/test/java/earth/maps/cardinal/data/navigation/PolylineDistanceCalculatorTest.kt @@ -0,0 +1,169 @@ +/* + * Cardinal Maps + * Copyright (C) 2026 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.data.navigation + +import earth.maps.cardinal.data.LatLng +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class PolylineDistanceCalculatorTest { + + private lateinit var calculator: PolylineDistanceCalculator + + @Before + fun setup() { + calculator = PolylineDistanceCalculator() + } + + // Helper + private fun latLng(lat: Double, lon: Double) = LatLng(lat, lon) + + @Test + fun `should return MAX_VALUE when polyline has less than 2 points`() { + val point = latLng(0.0, 0.0) + + val result = calculator.distanceFromPolyline(point, emptyList()) + + assertEquals(Double.MAX_VALUE, result, 0.0) + } + + @Test + fun `should return near zero when point lies on segment`() { + val polyline = listOf( + latLng(0.0, 0.0), + latLng(0.0, 1.0) + ) + + val point = latLng(0.0, 0.5) + + val distance = calculator.distanceFromPolyline(point, polyline) + + assertTrue(distance < 1.0) // < 1 meter + } + + @Test + fun `should calculate small distance when point is near segment`() { + val polyline = listOf( + latLng(0.0, 0.0), + latLng(0.0, 1.0) + ) + + val point = latLng(0.0001, 0.5) // ~11m away + + val distance = calculator.distanceFromPolyline(point, polyline) + + assertTrue(distance in 5.0..20.0) + } + + @Test + fun `should return large distance when point is far`() { + val polyline = listOf( + latLng(0.0, 0.0), + latLng(0.0, 1.0) + ) + + val point = latLng(1.0, 1.0) // ~100km away + + val distance = calculator.distanceFromPolyline(point, polyline) + + assertTrue(distance > 100_000) + } + + @Test + fun `should early exit when very close to segment`() { + val polyline = listOf( + latLng(0.0, 0.0), + latLng(0.0, 1.0), + latLng(1.0, 1.0) // extra segment + ) + + val point = latLng(0.0, 0.1) + + val distance = calculator.distanceFromPolyline(point, polyline) + + assertTrue(distance < 5.0) + } + + @Test + fun `should reuse last segment index for nearby points`() { + val polyline = listOf( + latLng(0.0, 0.0), + latLng(0.0, 1.0), + latLng(0.0, 2.0) + ) + + val point1 = latLng(0.0, 0.4) + val point2 = latLng(0.0, 0.5) + + val d1 = calculator.distanceFromPolyline(point1, polyline) + val d2 = calculator.distanceFromPolyline(point2, polyline) + + assertTrue(d1 < 5.0) + assertTrue(d2 < 5.0) + } + + @Test + fun `resetCache should reset segment index`() { + val polyline = listOf( + latLng(0.0, 0.0), + latLng(0.0, 1.0) + ) + + val point = latLng(0.0, 0.5) + + calculator.distanceFromPolyline(point, polyline) + + calculator.resetCache() + + val distance = calculator.distanceFromPolyline(point, polyline) + + assertTrue(distance < 5.0) + } + + @Test + fun `should handle degenerate segment safely`() { + val polyline = listOf( + latLng(0.0, 0.0), + latLng(0.0, 0.0) // same point + ) + + val point = latLng(0.1, 0.1) + + val distance = calculator.distanceFromPolyline(point, polyline) + + assertTrue(distance > 0) + } + + @Test + fun `should pick closest segment among multiple`() { + val polyline = listOf( + latLng(0.0, 0.0), + latLng(0.0, 1.0), + latLng(1.0, 1.0) + ) + + val point = latLng(0.0001, 0.8) + + val distance = calculator.distanceFromPolyline(point, polyline) + + assertTrue(distance < 20.0) + } +} diff --git a/cardinal-android/gradle/libs.versions.toml b/cardinal-android/gradle/libs.versions.toml index af49fb8..61105af 100644 --- a/cardinal-android/gradle/libs.versions.toml +++ b/cardinal-android/gradle/libs.versions.toml @@ -15,6 +15,7 @@ ktor = "3.3.3" lifecycleRuntimeKtx = "2.10.0" activityCompose = "1.12.1" composeBom = "2025.12.00" +loggingInterceptor = "5.3.2" maplibreCompose = "0.12.1" robolectric = "4.16" room = "2.8.4" @@ -65,6 +66,7 @@ ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "k ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibreCompose" } maplibre-compose-material3 = { module = "org.maplibre.compose:maplibre-compose-material3", version.ref = "maplibreCompose" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } -- GitLab