Skip to content

Commit

Permalink
Implement annotation publisher on Android (#324)
Browse files Browse the repository at this point in the history
* Implement annotation publisher on Android

This patch introduces an AnnotationPublisher to publish custom
annotations for Android. Fixes #316.

* Apply code review suggestions

* Convert comments to KDoc comments

---------

Co-authored-by: Ian Wagner <[email protected]>
  • Loading branch information
ahmedre and ianthetechie authored Nov 12, 2024
1 parent 2021ded commit f5790e2
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,6 @@ class FerrostarCore(
* @param route the route to navigate.
* @param config change the configuration in the core before staring navigation. This was
* originally provided on init, but you can set a new value for future sessions.
* @return a view model tied to the navigation session. This can be ignored if you're injecting
* the [NavigationViewModel]/[DefaultNavigationViewModel].
* @throws UserLocationUnknown if the location provider has no last known location.
*/
@Throws(UserLocationUnknown::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.stadiamaps.ferrostar.core
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.stadiamaps.ferrostar.core.annotation.AnnotationPublisher
import com.stadiamaps.ferrostar.core.annotation.NoOpAnnotationPublisher
import com.stadiamaps.ferrostar.core.extensions.currentRoadName
import com.stadiamaps.ferrostar.core.extensions.deviation
import com.stadiamaps.ferrostar.core.extensions.progress
Expand Down Expand Up @@ -104,14 +106,17 @@ interface NavigationViewModel {
*/
class DefaultNavigationViewModel(
private val ferrostarCore: FerrostarCore,
private val annotationPublisher: AnnotationPublisher<*> = NoOpAnnotationPublisher()
) : ViewModel(), NavigationViewModel {

private val muteState: StateFlow<Boolean?> =
ferrostarCore.spokenInstructionObserver?.muteState ?: MutableStateFlow(null)

override val uiState =
combine(ferrostarCore.state, muteState) { a, b -> a to b }
.map { (coreState, muteState) ->
.map { (coreState, muteState) -> annotationPublisher.map(coreState) to muteState }
.map { (stateWrapper, muteState) ->
val coreState = stateWrapper.state
val location = ferrostarCore.locationProvider.lastLocation
val userLocation =
when (coreState.tripState) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stadiamaps.ferrostar.core.annotation

import com.stadiamaps.ferrostar.core.NavigationState

interface AnnotationPublisher<T> {
fun map(state: NavigationState): AnnotationWrapper<T>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stadiamaps.ferrostar.core.annotation

import com.stadiamaps.ferrostar.core.NavigationState

data class AnnotationWrapper<T>(
val annotation: T? = null,
val speed: Speed? = null,
val state: NavigationState
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.stadiamaps.ferrostar.core.annotation

import com.squareup.moshi.JsonAdapter
import com.stadiamaps.ferrostar.core.NavigationState
import uniffi.ferrostar.TripState

class DefaultAnnotationPublisher<T>(
private val adapter: JsonAdapter<T>,
private val speedLimitMapper: (T?) -> Speed?,
) : AnnotationPublisher<T> {

override fun map(state: NavigationState): AnnotationWrapper<T> {
val annotations = decodeAnnotations(state)
return AnnotationWrapper(annotations, speedLimitMapper(annotations), state)
}

private fun decodeAnnotations(state: NavigationState): T? {
return if (state.tripState is TripState.Navigating) {
val json = state.tripState.annotationJson
if (json != null) {
adapter.fromJson(json)
} else {
null
}
} else {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stadiamaps.ferrostar.core.annotation

import com.stadiamaps.ferrostar.core.NavigationState

class NoOpAnnotationPublisher : AnnotationPublisher<Unit> {
override fun map(state: NavigationState): AnnotationWrapper<Unit> {
return AnnotationWrapper(state = state)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.stadiamaps.ferrostar.core.annotation

sealed class Speed {
data object NoLimit : Speed()

data object Unknown : Speed()

data class Value(val value: Double, val unit: SpeedUnit) : Speed()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.stadiamaps.ferrostar.core.annotation

import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson

class SpeedSerializationAdapter : JsonAdapter<Speed>() {

@ToJson
override fun toJson(writer: JsonWriter, speed: Speed?) {
if (speed == null) {
writer.nullValue()
} else {
writer.beginObject()
when (speed) {
is Speed.NoLimit -> writer.name("none").value(true)
is Speed.Unknown -> writer.name("unknown").value(true)
is Speed.Value ->
writer.name("value").value(speed.value).name("unit").value(speed.unit.text)
}
writer.endObject()
}
}

@FromJson
override fun fromJson(reader: JsonReader): Speed {
reader.beginObject()
var unknown: Boolean? = null
var none: Boolean? = null
var value: Double? = null
var unit: String? = null

while (reader.hasNext()) {
when (reader.selectName(JsonReader.Options.of("none", "unknown", "value", "unit"))) {
0 -> none = reader.nextBoolean()
1 -> unknown = reader.nextBoolean()
2 -> value = reader.nextDouble()
3 -> unit = reader.nextString()
else -> reader.skipName()
}
}
reader.endObject()

return if (none == true) {
Speed.NoLimit
} else if (unknown == true) {
Speed.Unknown
} else if (value != null && unit != null) {
val speed = SpeedUnit.fromString(unit)
if (speed != null) {
Speed.Value(value, speed)
} else {
throw IllegalArgumentException("Invalid speed unit: $unit")
}
} else {
throw IllegalArgumentException("Invalid max speed")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.stadiamaps.ferrostar.core.annotation

enum class SpeedUnit(val text: String) {
KILOMETERS_PER_HOUR("km/h"),
MILES_PER_HOUR("mph"),
KNOTS("knots");

companion object {
fun fromString(text: String): SpeedUnit? {
return SpeedUnit.entries.firstOrNull { it.text == text }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.stadiamaps.ferrostar.core.annotation.valhalla

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.stadiamaps.ferrostar.core.annotation.AnnotationPublisher
import com.stadiamaps.ferrostar.core.annotation.DefaultAnnotationPublisher
import com.stadiamaps.ferrostar.core.annotation.SpeedSerializationAdapter

fun valhallaExtendedOSRMAnnotationPublisher(): AnnotationPublisher<ValhallaOSRMExtendedAnnotation> {
val moshi =
Moshi.Builder().add(SpeedSerializationAdapter()).add(KotlinJsonAdapterFactory()).build()
val adapter = moshi.adapter(ValhallaOSRMExtendedAnnotation::class.java)
return DefaultAnnotationPublisher<ValhallaOSRMExtendedAnnotation>(adapter) { it?.speedLimit }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.stadiamaps.ferrostar.core.annotation.valhalla

import com.squareup.moshi.Json
import com.stadiamaps.ferrostar.core.annotation.Speed

data class ValhallaOSRMExtendedAnnotation(
/** The speed limit of the segment. */
@Json(name = "maxspeed") val speedLimit: Speed?,
/** The estimated speed of travel for the segment, in meters per second. */
val speed: Double?,
/** The distance in meters of the segment. */
val distance: Double?,
/** The estimated time to traverse the segment, in seconds. */
val duration: Double?
)

0 comments on commit f5790e2

Please sign in to comment.