Skip to content

Commit

Permalink
Implement annotation publisher on Android
Browse files Browse the repository at this point in the history
This patch introduces an AnnotationPublisher to publish custom
annotations for Android. Fixes #316.
  • Loading branch information
ahmedre committed Oct 30, 2024
1 parent dbee23a commit c6de6e4
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.stadiamaps.ferrostar.core
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.stadiamaps.ferrostar.core.annotation.valhalla.valhallaExtendedOSRMAnnotationPublisher
import com.stadiamaps.ferrostar.core.service.ForegroundServiceManager
import java.net.URL
import java.time.Instant
Expand Down Expand Up @@ -248,7 +249,11 @@ class FerrostarCore(

locationProvider.addListener(this, _executor)

return DefaultNavigationViewModel(this, spokenInstructionObserver, locationProvider)
return DefaultNavigationViewModel(
this,
spokenInstructionObserver,
locationProvider,
valhallaExtendedOSRMAnnotationPublisher())
}

/**
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.deviation
import com.stadiamaps.ferrostar.core.extensions.progress
import com.stadiamaps.ferrostar.core.extensions.visualInstruction
Expand Down Expand Up @@ -86,14 +88,17 @@ interface NavigationViewModel {
class DefaultNavigationViewModel(
private val ferrostarCore: FerrostarCore,
private val spokenInstructionObserver: SpokenInstructionObserver? = null,
private val locationProvider: LocationProvider
private val locationProvider: LocationProvider,
private val annotationPublisher: AnnotationPublisher<*> = NoOpAnnotationPublisher()
) : ViewModel(), NavigationViewModel {

private var userLocation: UserLocation? = locationProvider.lastLocation

override val uiState =
ferrostarCore.state
.map { coreState ->
.map { annotationPublisher.map(it) }
.map { stateWrapper ->
val coreState = stateWrapper.state
val location = locationProvider.lastLocation
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<Any> {
override fun map(state: NavigationState): AnnotationWrapper<Any> {
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,11 @@
package com.stadiamaps.ferrostar.core.annotation.valhalla

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

data class ValhallaOSRMExtendedAnnotation(
@Json(name = "maxspeed") val speedLimit: Speed?,
val speed: Double?,
val distance: Double?,
val duration: Double?
)

0 comments on commit c6de6e4

Please sign in to comment.