Merge pull request 'SDKv3' (#1) from mapbox-sdkv3 into mapbox

Reviewed-on: https://hattori.ztsh.eu/stawros/Garmin/pulls/1
This commit is contained in:
Piotr Dec 2024-08-03 01:57:01 +02:00
commit 06a9f12446
22 changed files with 1343 additions and 389 deletions

View file

@ -7,6 +7,7 @@ plugins {
android { android {
namespace 'eu.ztsh.garmin' namespace 'eu.ztsh.garmin'
compileSdk 34 compileSdk 34
ndkVersion "23.2.8568313"
defaultConfig { defaultConfig {
applicationId "eu.ztsh.garmin" applicationId "eu.ztsh.garmin"
@ -36,19 +37,24 @@ android {
viewBinding true viewBinding true
buildConfig = true buildConfig = true
} }
buildToolsVersion '35.0.0' }
ext {
mapboxVersion = '3.2.0'
} }
dependencies { dependencies {
implementation "com.mapbox.navigationcore:navigation:$mapboxVersion"
implementation "com.mapbox.navigation:ui-dropin:2.20.0" implementation "com.mapbox.navigationcore:ui-maps:$mapboxVersion"
implementation 'androidx.core:core-ktx:1.7.0' implementation "com.mapbox.navigationcore:voice:$mapboxVersion"
implementation 'androidx.appcompat:appcompat:1.4.1' implementation "com.mapbox.navigationcore:tripdata:$mapboxVersion"
implementation 'com.google.android.material:material:1.5.0' implementation "com.mapbox.navigationcore:ui-components:$mapboxVersion"
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'org.assertj:assertj-core:3.24.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
} }

View file

@ -6,13 +6,16 @@ import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
import android.util.Log import android.util.Log
import com.mapbox.navigation.core.trip.session.LocationMatcherResult import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.NavigationSessionState import com.mapbox.navigation.tripdata.maneuver.model.Maneuver
import com.mapbox.navigation.ui.maneuver.model.Maneuver import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue
import eu.ztsh.garmin.data.Arrows
import eu.ztsh.garmin.data.DataCache import eu.ztsh.garmin.data.DataCache
import eu.ztsh.garmin.data.GarminMapper import eu.ztsh.garmin.data.GarminMapper
import eu.ztsh.garmin.data.Lanes
import eu.ztsh.garmin.data.MapboxMapper import eu.ztsh.garmin.data.MapboxMapper
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.SynchronousQueue import java.util.concurrent.SynchronousQueue
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@ -23,68 +26,160 @@ class Garmin(
) { ) {
private lateinit var connection: ConnectThread private lateinit var connection: ConnectThread
private lateinit var processing: ProcessingThread private lateinit var maneuvers: ManeuverProcessingThread
private lateinit var trips: TripProgressProcessingThread
private lateinit var locations: LocationMatcherProcessingThread
private val cache = DataCache() private val cache = DataCache()
private val processingPool = Executors.newFixedThreadPool(8)
fun start() { fun start() {
connection = ConnectThread() connection = ConnectThread()
connection.start() connection.start()
maneuvers = ManeuverProcessingThread()
maneuvers.start()
trips = TripProgressProcessingThread()
trips.start()
locations = LocationMatcherProcessingThread()
locations.start()
} }
fun close() { fun close() {
connection.close() connection.close()
maneuvers.interrupt()
maneuvers.join(0)
trips.interrupt()
trips.join(0)
locations.interrupt()
locations.join(0)
processingPool.shutdown()
} }
fun process(maneuver: Maneuver) { fun process(maneuver: Maneuver) {
processing = ManeuverProcessingThread(maneuver) processingPool.submit{maneuvers.enqueue(maneuver)}
processing.start()
processing.join()
} }
fun process(location: LocationMatcherResult) { fun process(tripProgressUpdateValue: TripProgressUpdateValue) {
processing = LocationProcessingThread(location) processingPool.submit{trips.enqueue(tripProgressUpdateValue)}
processing.start()
processing.join()
} }
fun process(navigationSessionState: NavigationSessionState) { fun process(locationMatcherResult: LocationMatcherResult) {
cache.update(navigationSessionState) processingPool.submit{locations.enqueue(locationMatcherResult)}
} }
private inner class ManeuverProcessingThread(val maneuver: Maneuver) : ProcessingThread() { private inner class LocationMatcherProcessingThread: ProcessingThread<LocationMatcherResult>() {
override fun mapAndSend(maybeItem: LocationMatcherResult?): LocationMatcherResult? {
if (maybeItem != null && cache.hasChanged(maybeItem)) {
send(GarminMapper.map(MapboxMapper.asSpeed(maybeItem)))
return maybeItem
}
return null
}
override fun updateCache(item: LocationMatcherResult) {
cache.update(item)
}
}
private inner class TripProgressProcessingThread : ProcessingThread<TripProgressUpdateValue>() {
override fun mapAndSend(maybeItem: TripProgressUpdateValue?): TripProgressUpdateValue? {
if (maybeItem != null) {
// it is much simplier to parse and compare model object
val value = MapboxMapper.asEta(maybeItem)
if (cache.hasChanged(value)) {
// TODO: traffic
send(GarminMapper.setTime(value.hours, value.minutes))
cache.update(value)
}
}
return null
}
override fun updateCache(item: TripProgressUpdateValue) {
// won't be used
}
}
private inner class ManeuverProcessingThread : ProcessingThread<Maneuver>() {
override fun mapAndSend(maybeItem: Maneuver?): Maneuver? {
if (maybeItem != null) {
Log.d(TAG, "mapAndSend (${currentThread().name}): got new")
var changed = false
if (cache.hasChanged(maybeItem.primary)) {
changed = true
Log.d(TAG, "mapAndSend: primary")
send(GarminMapper.map(MapboxMapper.asDirection(maybeItem)))
if (maybeItem.laneGuidance == null) {
send(GarminMapper.map(Lanes(Arrows(setOf()), Arrows(setOf()))))
}
}
if (cache.hasChanged(maybeItem.laneGuidance)) {
changed = true
Log.d(TAG, "mapAndSend: lanes")
send(GarminMapper.map(MapboxMapper.asLanes(maybeItem)))
}
if (cache.hasChanged(maybeItem.stepDistance)) {
changed = true
Log.d(TAG, "mapAndSend: stepDistance")
send(GarminMapper.map(MapboxMapper.asDistance(maybeItem)))
}
if (changed) {
return maybeItem
}
}
return null
}
override fun updateCache(item: Maneuver) {
cache.update(item)
}
}
private abstract inner class ProcessingThread<T> : Thread() {
private val queue: SynchronousQueue<T> = SynchronousQueue()
private var stop: Boolean = false
abstract fun mapAndSend(maybeItem: T?): T?
abstract fun updateCache(item: T)
fun send(data: IntArray) {
connection.enqueue(data)
}
fun enqueue(item: T) {
queue.put(item)
}
override fun run() { override fun run() {
if (cache.hasChanged(maneuver)) { while (!stop) {
cache.update(maneuver) val maybeItem = queue.poll()
send(MapboxMapper.apply(maneuver)) val item = mapAndSend(maybeItem)
if (item != null) {
Log.d(TAG, "run: Cache updated")
updateCache(item)
}
} }
} }
} override fun interrupt() {
stop = true
private inner class LocationProcessingThread(val location: LocationMatcherResult) : ProcessingThread() { super.interrupt()
override fun run() {
if (cache.hasChanged(location)) {
cache.update(location)
send(MapboxMapper.apply(location))
}
} }
}
private open inner class ProcessingThread : Thread() {
fun send(incoming: eu.ztsh.garmin.data.State) {
if (cache.hasChanged(incoming.distance)) {
connection.enqueue(GarminMapper.setDistance(incoming))
}
if (cache.hasChanged(incoming.direction)) {
connection.enqueue(GarminMapper.setDirection(incoming))
}
cache.update(incoming)
}
} }
private inner class ConnectThread : Thread() { private inner class ConnectThread : Thread() {
@ -106,10 +201,12 @@ class Garmin(
context.setConnectionStatus(true) context.setConnectionStatus(true)
sleep(3000) sleep(3000)
readAll() readAll()
send(intArrayOf(0x07, 0x01)) // Set GPS to true
while (true) { while (true) {
val newCurrent = queue.poll() val newCurrent = queue.poll()
if (newCurrent == null) { if (newCurrent == null) {
sleep(500) Log.d(TAG, "run (${currentThread().name}): Sleep...")
sleep(250)
} else { } else {
current = newCurrent current = newCurrent
} }
@ -157,13 +254,15 @@ class Garmin(
private fun sendRaw(buff: IntArray) { private fun sendRaw(buff: IntArray) {
buff.forEach { socket!!.outputStream.write(it) } buff.forEach { socket!!.outputStream.write(it) }
socket!!.outputStream.flush() socket!!.outputStream.flush()
sleep(2000)
readAll() readAll()
} }
} }
companion object { companion object {
lateinit var instance: Garmin
fun prepareData(input: IntArray): IntArray { fun prepareData(input: IntArray): IntArray {
val n = input.size val n = input.size
var crc = (0xeb + n + n).toUInt() var crc = (0xeb + n + n).toUInt()

View file

@ -5,57 +5,76 @@ import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
import com.mapbox.navigation.base.options.NavigationOptions import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.trip.session.RouteProgressObserver import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi import com.mapbox.navigation.core.lifecycle.requireMapboxNavigation
import eu.ztsh.garmin.databinding.ActivityMainBinding import eu.ztsh.garmin.databinding.ActivityMainBinding
import eu.ztsh.garmin.mapbox.MapControl
import eu.ztsh.garmin.util.PermissionsHelper
import java.lang.ref.WeakReference
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
lateinit var garmin: Garmin // lateinit var garmin: Garmin
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private val mapboxToolbox = MapboxToolbox(lifecycle, this) private lateinit var mapControl: MapControl
private val permissionsHelper = PermissionsHelper(WeakReference(this))
val mapboxNavigation: MapboxNavigation by requireMapboxNavigation(
onResumedObserver = object : DefaultLifecycleObserver, MapboxNavigationObserver {
override fun onAttached(mapboxNavigation: MapboxNavigation) {
mapControl.onAttached(mapboxNavigation)
}
override fun onDetached(mapboxNavigation: MapboxNavigation) {
mapControl.onDetached(mapboxNavigation)
}
},
onInitialize = fun() {
mapControl.initNavigation()
}
)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
mapboxToolbox.onCreate() binding.mapView
permissionsHelper.checkPermissions {
mapControl = MapControl(this, UI(binding), resources)
mapControl.init()
MapboxNavigationApp.setup(
NavigationOptions.Builder(applicationContext)
.build()
)
}
bluetoothInit() bluetoothInit()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
override fun onStart() {
super.onStart()
mapboxToolbox.onStart()
}
override fun onStop() {
super.onStop()
mapboxToolbox.onStop()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
mapboxToolbox.onDestroy() mapControl.onDestroy()
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
private fun bluetoothInit() { private fun bluetoothInit() {
@ -74,10 +93,10 @@ class MainActivity : AppCompatActivity() {
val context = this val context = this
pairedDevices?.firstOrNull { device -> pairedDevices?.firstOrNull { device ->
Log.d(TAG, device.name) Log.d(TAG, device.name)
device.name.equals("GARMIN HUD") device.name == "GARMIN HUD"
}?.apply { }?.apply {
garmin = Garmin(context, this, bluetoothAdapter) Garmin.instance = Garmin(context, this, bluetoothAdapter)
garmin.start() Garmin.instance.start()
} }
} }

View file

@ -1,21 +0,0 @@
package eu.ztsh.garmin
import android.util.Log
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
class MapboxObserver : MapboxNavigationObserver {
override fun onAttached(mapboxNavigation: MapboxNavigation) {
Log.d(TAG, "Attached")
}
override fun onDetached(mapboxNavigation: MapboxNavigation) {
Log.d(TAG, "Detached")
}
companion object {
const val TAG = "MBOXOBS"
}
}

View file

@ -1,96 +0,0 @@
package eu.ztsh.garmin
import android.annotation.SuppressLint
import android.location.Location
import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.core.trip.session.NavigationSessionState
import com.mapbox.navigation.core.trip.session.NavigationSessionStateObserver
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi
@SuppressLint("MissingPermission")
class MapboxToolbox(lifecycle: Lifecycle, private val context: MainActivity) {
private val mapboxObserver = MapboxObserver()
init {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
MapboxNavigationApp.attach(owner)
MapboxNavigationApp.registerObserver(mapboxObserver)
}
override fun onPause(owner: LifecycleOwner) {
MapboxNavigationApp.detach(owner)
MapboxNavigationApp.unregisterObserver(mapboxObserver)
}
})
}
fun onCreate() {
if (!MapboxNavigationApp.isSetup()) {
MapboxNavigationApp.setup {
NavigationOptions.Builder(context)
.accessToken(BuildConfig.MAPBOX_DOWNLOADS_TOKEN)
.build()
}
}
MapboxNavigationApp.current()?.startTripSession()
}
fun onStart() {
MapboxNavigationApp.current()?.registerRouteProgressObserver(routeProgressObserver)
MapboxNavigationApp.current()?.registerLocationObserver(locationObserver)
MapboxNavigationApp.current()?.registerNavigationSessionStateObserver(navigationStateObserver)
}
fun onStop() {
MapboxNavigationApp.current()?.unregisterRouteProgressObserver(routeProgressObserver)
MapboxNavigationApp.current()?.unregisterLocationObserver(locationObserver)
MapboxNavigationApp.current()?.unregisterNavigationSessionStateObserver(navigationStateObserver)
}
fun onDestroy() {
MapboxNavigationApp.current()?.stopTripSession()
maneuverApi.cancel()
}
// Define distance formatter options
private val distanceFormatter: DistanceFormatterOptions by lazy {
DistanceFormatterOptions.Builder(context).build()
}
// Create an instance of the Maneuver API
private val maneuverApi: MapboxManeuverApi by lazy {
MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatter))
}
private val routeProgressObserver =
RouteProgressObserver { routeProgress ->
maneuverApi.getManeuvers(routeProgress).value?.apply {
context.garmin.process(
this[0]
)
}
}
private val locationObserver = object : LocationObserver {
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
context.garmin.process(locationMatcherResult)
}
override fun onNewRawLocation(rawLocation: Location) {
}
}
private val navigationStateObserver = NavigationSessionStateObserver { context.garmin.process(it) }
}

View file

@ -0,0 +1,59 @@
package eu.ztsh.garmin
import android.content.res.Resources
import com.mapbox.maps.EdgeInsets
import eu.ztsh.garmin.databinding.ActivityMainBinding
class UI(b: ActivityMainBinding) {
companion object {
const val BUTTON_ANIMATION_DURATION = 1500L
private val pixelDensity = Resources.getSystem().displayMetrics.density
val overviewPadding: EdgeInsets by lazy {
EdgeInsets(
140.0 * pixelDensity,
40.0 * pixelDensity,
120.0 * pixelDensity,
40.0 * pixelDensity
)
}
val landscapeOverviewPadding: EdgeInsets by lazy {
EdgeInsets(
30.0 * pixelDensity,
380.0 * pixelDensity,
110.0 * pixelDensity,
20.0 * pixelDensity
)
}
val followingPadding: EdgeInsets by lazy {
EdgeInsets(
180.0 * pixelDensity,
40.0 * pixelDensity,
150.0 * pixelDensity,
40.0 * pixelDensity
)
}
val landscapeFollowingPadding: EdgeInsets by lazy {
EdgeInsets(
30.0 * pixelDensity,
380.0 * pixelDensity,
110.0 * pixelDensity,
40.0 * pixelDensity
)
}
}
val mapView = b.mapView
val maneuverView = b.maneuverView
val tripProgressView = b.tripProgressView
val tripProgressCard = b.tripProgressCard
val recenter = b.recenter
val soundButton = b.soundButton
val routeOverview = b.routeOverview
val stop = b.stop
}

View file

@ -2,74 +2,49 @@ package eu.ztsh.garmin.data
import com.mapbox.navigation.core.trip.session.LocationMatcherResult import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.NavigationSessionState import com.mapbox.navigation.core.trip.session.NavigationSessionState
import com.mapbox.navigation.ui.maneuver.model.Maneuver import com.mapbox.navigation.tripdata.maneuver.model.Lane
import com.mapbox.navigation.tripdata.maneuver.model.Maneuver
import com.mapbox.navigation.tripdata.maneuver.model.PrimaryManeuver
import com.mapbox.navigation.tripdata.maneuver.model.StepDistance
class DataCache { class DataCache {
private val stateCache: State = State()
private var maneuverCache: Maneuver? = null private var maneuverCache: Maneuver? = null
private var locationCache: LocationMatcherResult? = null private var locationCache: LocationMatcherResult? = null
private var session: NavigationSessionState? = null private var session: NavigationSessionState? = null
private var eta: Arrival? = null
// state
fun hasChanged(lanes: Lanes?): Boolean {
return stateCache.lineArrows == null || stateCache.lineArrows != lanes
}
fun hasChanged(outlines: Outlines?): Boolean {
return stateCache.lineOutlines == null || stateCache.lineOutlines != outlines
}
fun hasChanged(distance: Distance?): Boolean {
return stateCache.distance == null || stateCache.distance != distance
}
fun hasChanged(direction: Direction?): Boolean {
return stateCache.direction == null || stateCache.direction != direction
}
fun hasChanged(speed: Speed?): Boolean {
return stateCache.speed == null || stateCache.speed != speed
}
fun hasChanged(arrival: Arrival?): Boolean {
return stateCache.arrival == null || stateCache.arrival != arrival
}
// Merge states
fun update(state: State) {
if (state.lineArrows != null) {
stateCache.lineArrows = state.lineArrows
}
if (state.lineOutlines != null) {
state.lineOutlines = state.lineOutlines
}
if (state.direction != null) {
stateCache.direction = state.direction
}
if (state.distance != null) {
stateCache.distance = state.distance
}
if (state.speed != null) {
stateCache.speed = state.speed
}
if (state.arrival != null) {
stateCache.arrival = state.arrival
}
}
// maneuver // maneuver
fun hasChanged(maneuver: Maneuver): Boolean { fun hasChanged(guidance: Lane?): Boolean {
return maneuverCache == null || maneuverCache!! != maneuver return guidance != null && maneuverCache.let { it == null || it.laneGuidance != guidance }
}
fun hasChanged(distance: StepDistance): Boolean {
return maneuverCache.let { it == null || it.stepDistance != distance }
}
fun hasChanged(primaryManeuver: PrimaryManeuver): Boolean {
return maneuverCache.let { it == null || it.primary != primaryManeuver }
} }
fun update(maneuver: Maneuver) { fun update(maneuver: Maneuver) {
maneuverCache = maneuver maneuverCache = maneuver
} }
// eta
fun hasChanged(eta: Arrival): Boolean {
return this.eta.let { it == null || it != eta }
}
fun update(eta: Arrival) {
this.eta = eta
}
// location // location
fun hasChanged(locationMatcherResult: LocationMatcherResult): Boolean { fun hasChanged(locationMatcherResult: LocationMatcherResult): Boolean {
return locationCache == null || locationCache!! != locationMatcherResult return locationCache.let { it == null
|| it.enhancedLocation.speed != locationMatcherResult.enhancedLocation.speed
|| it.speedLimitInfo.speed != locationMatcherResult.speedLimitInfo.speed }
} }
fun update(locationMatcherResult: LocationMatcherResult) { fun update(locationMatcherResult: LocationMatcherResult) {

View file

@ -4,29 +4,26 @@ class GarminMapper {
companion object { companion object {
fun setLines(state: State): IntArray { fun map(lanes: Lanes): IntArray {
return intArrayOf(0x02, state.lineOutlines.sumOf { it.value }, state.lineArrows.sumOf { it.value }) return intArrayOf(0x02, lanes.lanes.lanes.sumOf { it.value }, lanes.outlines.lanes.sumOf { it.value })
} }
fun setDirection(state: State): IntArray { fun map(direction: Direction): IntArray {
return setDirection(state.direction.angle, state.direction.out, state.direction.roundabout) return toDirectionArray(direction.angle, direction.out, direction.roundabout)
} }
fun setDistance(state: State): IntArray { fun map(distance: Distance): IntArray {
return setDistance(state.distance, state.unit) return setDistance(distance.distance, distance.unit)
} }
fun setSpeed(state: State): IntArray { fun map(speed: Speed): IntArray {
return setSpeed(state.speed, state.limit, state.speed > state.limit) return setSpeed(speed.speed, speed.limit)
}
fun setSpeedFreeRide(state: State): Pair<IntArray, IntArray> {
return Pair(setDistance(state.speed), setSpeed(state.limit, limitWarning = state.speed > state.limit))
} }
fun setTime(hours: Int, minutes: Int, traffic: Boolean = false, flag: Boolean = false): IntArray { fun setTime(hours: Int, minutes: Int, traffic: Boolean = false, flag: Boolean = false): IntArray {
val trafficChar = asChar(traffic) val trafficChar = asChar(traffic)
val flagChar = asChar(flag) val flagChar = asChar(flag)
// TODO: needed? Displaying ETA, not remaining
return if (hours > 99) { return if (hours > 99) {
intArrayOf( intArrayOf(
0x05, 0x05,
@ -44,7 +41,7 @@ class GarminMapper {
0x05, 0x05,
trafficChar, trafficChar,
asDigit(hours / 10), asDigit(hours / 10),
asDigit(hours), asDigit(if (hours == 0) 10 else hours),
0xff, 0xff,
asDigit(minutes / 10), asDigit(minutes / 10),
asDigit(minutes), asDigit(minutes),
@ -54,20 +51,15 @@ class GarminMapper {
} }
} }
fun setSpeedControl(state: State): IntArray { fun speedControl(state: Boolean): IntArray {
return intArrayOf(0x04, if (state.control) 0x01 else 0x02) return intArrayOf(0x04, if (state) 0x01 else 0x02)
}
fun setCompass(state: State): IntArray {
// TODO: Implement
return setDirection(OutAngle.Straight, OutType.ArrowOnly)
} }
fun cleanDistance(): IntArray { fun cleanDistance(): IntArray {
return intArrayOf(0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) return intArrayOf(0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
} }
private fun setDirection( private fun toDirectionArray(
angle: OutAngle, angle: OutAngle,
out: OutType = OutType.Lane, out: OutType = OutType.Lane,
roundabout: OutAngle = OutAngle.AsDirection roundabout: OutAngle = OutAngle.AsDirection
@ -89,33 +81,38 @@ class GarminMapper {
return intArrayOf(0x01, param1, param2, param3) return intArrayOf(0x01, param1, param2, param3)
} }
private fun setDistance(distance: Int, unit: Unit = Unit.Any): IntArray { private fun setDistance(distance: Double, unit: Unit = Unit.Any): IntArray {
val isDecimal = (distance * 10).toInt() != (distance.toInt() * 10)
val distanceValue = if (isDecimal) distance * 10 else distance
return intArrayOf( return intArrayOf(
0x03, asDigit(distance / 1000), asDigit(distance / 100), asDigit(distance / 10), 0x03,
0x00, asDigit(distance), unit.value asDigit(distanceValue / 1000), // position 1
asDigit(distanceValue / 100), // position 2
asDigit(distanceValue / 10), // position 3
if (isDecimal) 0xff else 0x00, // comma
asDigit(distanceValue), // position 4
unit.value // unit
) )
} }
private fun setSpeed( private fun setSpeed(
speed: Int, speed: Int,
limit: Int = 0, limit: Int = 0,
limitWarning: Boolean = false,
acc: Boolean = false acc: Boolean = false
): IntArray { ): IntArray {
// TODO: car connection // TODO: car connection
val accChar = asChar(acc) val accChar = asChar(acc)
val limitWarningChar = asChar(limitWarning)
return if (limit > 0) { return if (limit > 0) {
intArrayOf( intArrayOf(
0x06, 0x06,
asDigit(speed / 100), asDigit(speed / 100),
asDigit(speed / 10), asDigit(speed / 10),
asDigit(speed), asDigit(if (speed == 0) 10 else speed),
0xff, 0xff,
asDigit(limit / 100), asDigit(limit / 100),
asDigit(limit / 10), asDigit(limit / 10),
asDigit(limit), asDigit(limit),
limitWarningChar, asChar(speed > limit),
accChar accChar
) )
} else { } else {
@ -128,12 +125,16 @@ class GarminMapper {
asDigit(speed / 100), asDigit(speed / 100),
asDigit(speed / 10), asDigit(speed / 10),
asDigit(speed), asDigit(speed),
limitWarningChar, asChar(false),
accChar accChar
) )
} }
} }
private fun asDigit(n: Double): Int {
return asDigit(n.toInt())
}
private fun asDigit(n: Int): Int { private fun asDigit(n: Int): Int {
if (n == 0) { if (n == 0) {
return 0 return 0

View file

@ -1,74 +1,122 @@
package eu.ztsh.garmin.data package eu.ztsh.garmin.data
import com.mapbox.navigation.tripdata.maneuver.model.Maneuver
import com.mapbox.navigation.core.trip.session.LocationMatcherResult import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.ui.maneuver.model.Maneuver import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue
class MapboxMapper { class MapboxMapper {
companion object { companion object {
fun apply(maneuver: Maneuver): State { fun asDirection(maneuver: Maneuver): Direction {
val state = State() val direction = Direction()
maneuver.apply { maneuver.primary.apply {
this.primary.apply { when (this.type) {
state.direction = Direction() "roundabout" -> {
when (this.type) { direction.out = OutType.RightRoundabout
"roundabout" -> {
state.direction!!.out = OutType.RightRoundabout
}
"arrive" -> {
state.flag = true
}
} }
when (this.modifier) { "arrive" -> {
"right" -> { direction.out = OutType.Flag
when (this.type) { direction.angle = OutAngle.Right
"turn" -> state.direction!!.angle = OutAngle.Right }
"roundabout" -> { "turn" -> {
when (this.degrees) { when (this.modifier) {
137.0 -> state.direction!!.angle = OutAngle.EasyRight "straight" -> direction.angle = OutAngle.Straight
180.0 -> state.direction!!.angle = OutAngle.Straight "slight right" -> direction.angle = OutAngle.EasyRight
} "slight left" -> direction.angle = OutAngle.EasyLeft
} "sharp right" -> direction.angle = OutAngle.SharpRight
} "sharp left" -> direction.angle = OutAngle.SharpLeft
} "uturn" -> direction.angle = OutAngle.LeftDown
"left" -> {
when (this.type) {
"turn" -> state.direction!!.angle = OutAngle.Left
}
} }
} }
} }
this.stepDistance.apply { when (this.modifier) {
this.distanceRemaining?.apply { "right" -> {
distanceFormatter.formatDistance(distanceRemaining!!).split(" ").apply { direction.angle = OutAngle.Right
state.distance = Distance( when (this.type) {
this[0].replace(',', '.').toDouble().toInt(), "roundabout" -> {
when (this[1]) { when (this.degrees!!.toInt()) {
"m" -> Unit.Metres in 22..66 -> direction.angle = OutAngle.SharpRight
"km" -> Unit.Kilometres in 67..111 -> direction.angle = OutAngle.Right
else -> Unit.Any in 112..156 -> direction.angle = OutAngle.EasyRight
in 157..203 -> direction.angle = OutAngle.Straight
in 204..248 -> direction.angle = OutAngle.EasyLeft
in 249..293 -> direction.angle = OutAngle.Left
in 294..338 -> direction.angle = OutAngle.SharpLeft
else -> direction.angle = OutAngle.Down
} }
) }
"fork", "off ramp" -> {
direction.angle = OutAngle.EasyRight
direction.out = OutType.LongerLane
}
} }
} }
}
this.laneGuidance?.apply { "left" -> {
this.allLanes.apply { direction.angle = OutAngle.Left
println() when (this.type) {
"fork", "off ramp" -> {
direction.angle = OutAngle.EasyLeft
direction.out = OutType.LongerLane
}
}
} }
} }
} }
return state return direction
} }
fun apply(locationMatcherResult: LocationMatcherResult): State { fun asDistance(maneuver: Maneuver): Distance {
val state = State() return maneuver.stepDistance.let { step ->
// TODO: speed, limit, location?, bearing step.distanceFormatter.formatDistance(step.distanceRemaining!!).split(" ").let {
return state Distance(
it[0].replace(',', '.').toDouble(),
when (it[1]) {
"m" -> Unit.Metres
"km" -> Unit.Kilometres
else -> Unit.Any
}
)
}
}
}
fun asLanes(maneuver: Maneuver): Lanes {
val laneIterator = Lane.iterator()
val outlines = mutableSetOf<Lane>()
val lanes = mutableSetOf<Lane>()
maneuver.laneGuidance?.apply {
this.allLanes.reversed().let {
it.forEach{ indicator ->
val lane = if (laneIterator.hasNext()) laneIterator.next() else Lane.DotsLeft
if (lane == Lane.DotsLeft) {
outlines.add(Lane.DotsLeft)
} else {
outlines.add(lane)
if (indicator.isActive) {
lanes.add(lane)
}
}
}
}
}
return Lanes(Arrows(lanes), Arrows(outlines))
}
fun asSpeed(locationMatcherResult: LocationMatcherResult): Speed {
return Speed(
locationMatcherResult.enhancedLocation.speed.let { it?.toInt() ?: 0 },
locationMatcherResult.speedLimitInfo.speed.let { it ?: 0 },
)
}
fun asEta(trip: TripProgressUpdateValue): Arrival {
val eta = trip.formatter
.getEstimatedTimeToArrival(trip.estimatedTimeToArrival)
.toString().split(":")
return Arrival(eta[0].toInt(), eta[1].toInt())
} }
} }

View file

@ -12,7 +12,6 @@ enum class OutType(val value: Int) {
} }
enum class OutAngle(val value: Int) { enum class OutAngle(val value: Int) {
Down(0x01), Down(0x01),
@ -48,11 +47,15 @@ enum class Lane(val value: Int) {
InnerLeft(0x10), InnerLeft(0x10),
MiddleLeft(0x20), MiddleLeft(0x20),
OuterLeft(0x40), OuterLeft(0x40),
DotsLeft(0x80) DotsLeft(0x80);
companion object {
val iterator = {sortedSetOf(OuterRight, MiddleRight, InnerRight, InnerLeft, MiddleLeft, OuterLeft).iterator()}
}
} }
open class Arrows(val lanes: List<Lane>) { class Arrows(val lanes: Set<Lane>) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -68,11 +71,9 @@ open class Arrows(val lanes: List<Lane>) {
} }
} }
class Lanes(lanes: List<Lane>) : Arrows(lanes) class Lanes(val outlines: Arrows, val lanes: Arrows)
class Outlines(lanes: List<Lane>) : Arrows(lanes) class Distance(val distance: Double, val unit: Unit) {
class Distance(val distance: Int, val unit: Unit) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -87,37 +88,44 @@ class Distance(val distance: Int, val unit: Unit) {
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = distance var result = distance.hashCode()
result = 31 * result + unit.hashCode() result = 31 * result + unit.hashCode()
return result return result
} }
override fun toString(): String {
return "Distance($distance$unit)"
}
} }
class Speed(val speed: Int, val limit: Int) class Speed(val speed: Int, val limit: Int)
class Arrival(val hours: Int, val minutes: Int) class Arrival(val hours: Int, val minutes: Int) {
class State { override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
var lineArrows: Lanes? = null other as Arrival
var lineOutlines: Outlines? = null
var direction : Direction? = null
var distance: Distance? = null
var speed: Speed? = null
var arrival: Arrival? = null
// TODO: Bearing
// TODO: support
var traffic: Boolean? = null
var flag: Boolean? = null
var control: Boolean? = null
if (hours != other.hours) return false
if (minutes != other.minutes) return false
return true
}
override fun hashCode(): Int {
return 61 * hours + minutes
}
} }
class Direction { class Direction(
var angle: OutAngle = OutAngle.AsDirection var angle: OutAngle = OutAngle.AsDirection,
var out: OutType = OutType.Lane var out: OutType = OutType.Lane,
var roundabout: OutAngle = OutAngle.AsDirection var roundabout: OutAngle = OutAngle.AsDirection
) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View file

@ -0,0 +1,49 @@
package eu.ztsh.garmin.mapbox
import com.mapbox.common.location.Location
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
import eu.ztsh.garmin.Garmin
class LocationObserver(private val mapControl: MapControl) : LocationObserver {
/**
* Gets notified with location updates.
*
* Exposes raw updates coming directly from the location services
* and the updates enhanced by the Navigation SDK (cleaned up and matched to the road).
*/
private var firstLocationUpdateReceived = false
override fun onNewRawLocation(rawLocation: Location) {
// not handled
}
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
val enhancedLocation = locationMatcherResult.enhancedLocation
// update location puck's position on the map
mapControl.navigationLocationProvider.changePosition(
location = enhancedLocation,
keyPoints = locationMatcherResult.keyPoints,
)
// update camera position to account for new location
mapControl.viewportDataSource.onLocationChanged(enhancedLocation)
mapControl.viewportDataSource.evaluate()
Garmin.instance.process(locationMatcherResult)
// if this is the first location update the activity has received,
// it's best to immediately move the camera to the current user location
if (!firstLocationUpdateReceived) {
firstLocationUpdateReceived = true
mapControl.navigationCamera.requestNavigationCameraToOverview(
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
.maxDuration(0) // instant transition
.build()
)
}
}
}

View file

@ -0,0 +1,158 @@
package eu.ztsh.garmin.mapbox
import android.content.res.Configuration
import android.content.res.Resources
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.maps.ImageHolder
import com.mapbox.maps.plugin.LocationPuck2D
import com.mapbox.maps.plugin.animation.camera
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
import com.mapbox.navigation.ui.maps.camera.NavigationCamera
import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
import com.mapbox.navigation.ui.maps.camera.lifecycle.NavigationBasicGesturesHandler
import com.mapbox.navigation.ui.maps.camera.state.NavigationCameraState
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
import eu.ztsh.garmin.MainActivity
import eu.ztsh.garmin.UI
import eu.ztsh.garmin.mock.ReplayResources
class MapControl(
val context: AppCompatActivity,
val ui: UI,
private val resources: Resources
) : MapboxNavigationObserver {
/**
* Used to execute camera transitions based on the data generated by the [viewportDataSource].
* This includes transitions from route overview to route following and continuously updating the camera as the location changes.
*/
lateinit var navigationCamera: NavigationCamera
/**
* Produces the camera frames based on the location and routing data for the [navigationCamera] to execute.
*/
lateinit var viewportDataSource: MapboxNavigationViewportDataSource
/**
* [NavigationLocationProvider] is a utility class that helps to provide location updates generated by the Navigation SDK
* to the Maps SDK in order to update the user location indicator on the map.
*/
val navigationLocationProvider = NavigationLocationProvider()
val replay = ReplayResources(this)
// Observers
private lateinit var routeControl: RouteControl
private lateinit var voiceControl: VoiceControl
private lateinit var routesObserver: RoutesObserver
private lateinit var locationObserver: LocationObserver
private lateinit var routeProgressObserver: RouteProgressObserver
private lateinit var voiceInstructionsObserver: VoiceInstructionsObserver
fun init() {
viewportDataSource = MapboxNavigationViewportDataSource(ui.mapView.mapboxMap)
if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
viewportDataSource.overviewPadding = UI.landscapeOverviewPadding
} else {
viewportDataSource.overviewPadding = UI.overviewPadding
}
if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
viewportDataSource.followingPadding = UI.landscapeFollowingPadding
} else {
viewportDataSource.followingPadding = UI.followingPadding
}
navigationCamera = NavigationCamera(
ui.mapView.mapboxMap,
ui.mapView.camera,
viewportDataSource
)
// set the animations lifecycle listener to ensure the NavigationCamera stops
// automatically following the user location when the map is interacted with
ui.mapView.camera.addCameraAnimationsLifecycleListener(
NavigationBasicGesturesHandler(navigationCamera)
)
navigationCamera.registerNavigationCameraStateChangeObserver { navigationCameraState ->
// shows/hide the recenter button depending on the camera state
when (navigationCameraState) {
NavigationCameraState.TRANSITION_TO_FOLLOWING,
NavigationCameraState.FOLLOWING -> ui.recenter.visibility = View.INVISIBLE
NavigationCameraState.TRANSITION_TO_OVERVIEW,
NavigationCameraState.OVERVIEW,
NavigationCameraState.IDLE -> ui.recenter.visibility = View.VISIBLE
}
}
routeControl = RouteControl(this, ui, context)
voiceControl = VoiceControl(ui, context)
routesObserver = routeControl.routesObserver
locationObserver = LocationObserver(this)
routeProgressObserver = routeControl.routeProgressObserver
voiceInstructionsObserver = voiceControl.voiceInstructionsObserver
}
fun initNavigation() {
MapboxNavigationApp.setup(
NavigationOptions.Builder(context)
.build()
)
// initialize location puck
ui.mapView.location.apply {
setLocationProvider(navigationLocationProvider)
this.locationPuck = LocationPuck2D(
bearingImage = ImageHolder.Companion.from(
com.mapbox.navigation.ui.maps.R.drawable.mapbox_navigation_puck_icon
)
)
puckBearingEnabled = true
enabled = true
}
replay.replayOriginLocation()
}
fun mapboxNavigation(): MapboxNavigation {
return (context as MainActivity).mapboxNavigation
}
override fun onAttached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.registerRoutesObserver(routesObserver)
mapboxNavigation.registerLocationObserver(locationObserver)
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
replay.onAttached(mapboxNavigation)
}
override fun onDetached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.unregisterRoutesObserver(routesObserver)
mapboxNavigation.unregisterLocationObserver(locationObserver)
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
replay.onDetached(mapboxNavigation)
}
fun onDestroy() {
routeControl.cancel()
voiceControl.cancel()
}
companion object {
const val TAG = "MAPCTRL"
}
}

View file

@ -0,0 +1,309 @@
package eu.ztsh.garmin.mapbox
import android.content.Context
import android.view.View
import android.widget.Toast
import com.mapbox.api.directions.v5.models.Bearing
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.geojson.Point
import com.mapbox.maps.plugin.gestures.gestures
import com.mapbox.navigation.base.TimeFormat
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.route.NavigationRouterCallback
import com.mapbox.navigation.base.route.RouterFailure
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi
import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi
import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter
import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalFormatter
import com.mapbox.navigation.tripdata.progress.model.PercentDistanceTraveledFormatter
import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter
import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter
import com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
import com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView
import com.mapbox.navigation.ui.maps.NavigationStyles
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView
import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
import eu.ztsh.garmin.Garmin
import eu.ztsh.garmin.UI
class RouteControl(private val mapControl: MapControl, ui: UI, private val context: Context) {
/**
* Generates updates for the [MapboxManeuverView] to display the upcoming maneuver instructions
* and remaining distance to the maneuver point.
*/
private lateinit var maneuverApi: MapboxManeuverApi
/**
* Generates updates for the [MapboxTripProgressView] that include remaining time and distance to the destination.
*/
private lateinit var tripProgressApi: MapboxTripProgressApi
/**
* Generates updates for the [routeLineView] with the geometries and properties of the routes that should be drawn on the map.
*/
private lateinit var routeLineApi: MapboxRouteLineApi
/**
* Draws route lines on the map based on the data from the [routeLineApi]
*/
private lateinit var routeLineView: MapboxRouteLineView
/**
* Generates updates for the [routeArrowView] with the geometries and properties of maneuver arrows that should be drawn on the map.
*/
private val routeArrowApi: MapboxRouteArrowApi = MapboxRouteArrowApi()
/**
* Draws maneuver arrows on the map based on the data [routeArrowApi].
*/
private lateinit var routeArrowView: MapboxRouteArrowView
init {
// make sure to use the same DistanceFormatterOptions across different features
val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
maneuverApi = MapboxManeuverApi(
MapboxDistanceFormatter(distanceFormatterOptions)
)
// initialize bottom progress view
tripProgressApi = MapboxTripProgressApi(
TripProgressUpdateFormatter.Builder(context)
.distanceRemainingFormatter(
DistanceRemainingFormatter(distanceFormatterOptions)
)
.timeRemainingFormatter(
TimeRemainingFormatter(context)
)
.percentRouteTraveledFormatter(
PercentDistanceTraveledFormatter()
)
.estimatedTimeToArrivalFormatter(
EstimatedTimeToArrivalFormatter(context, TimeFormat.TWENTY_FOUR_HOURS)
)
.build()
)
// initialize route line, the routeLineBelowLayerId is specified to place
// the route line below road labels layer on the map
// the value of this option will depend on the style that you are using
// and under which layer the route line should be placed on the map layers stack
val mapboxRouteLineViewOptions = MapboxRouteLineViewOptions.Builder(context)
.routeLineBelowLayerId("road-label-navigation")
.build()
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
routeLineView = MapboxRouteLineView(mapboxRouteLineViewOptions)
// initialize maneuver arrow view to draw arrows on the map
val routeArrowOptions = RouteArrowOptions.Builder(context).build()
routeArrowView = MapboxRouteArrowView(routeArrowOptions)
// load map style
ui.mapView.mapboxMap.loadStyle(NavigationStyles.NAVIGATION_DAY_STYLE) {
// Ensure that the route line related layers are present before the route arrow
routeLineView.initializeLayers(it)
// add long click listener that search for a route to the clicked destination
ui.mapView.gestures.addOnMapLongClickListener { point ->
findRoute(point)
true
}
}
// initialize view interactions
ui.stop.setOnClickListener {
clearRouteAndStopNavigation()
}
ui.recenter.setOnClickListener {
mapControl.navigationCamera.requestNavigationCameraToFollowing()
ui.routeOverview.showTextAndExtend(UI.BUTTON_ANIMATION_DURATION)
}
ui.routeOverview.setOnClickListener {
mapControl.navigationCamera.requestNavigationCameraToOverview()
ui.recenter.showTextAndExtend(UI.BUTTON_ANIMATION_DURATION)
}
// set initial sounds button state
ui.soundButton.unmute()
}
/**
* Gets notified with progress along the currently active route.
*/
val routeProgressObserver = RouteProgressObserver { routeProgress ->
// update the camera position to account for the progressed fragment of the route
mapControl.viewportDataSource.onRouteProgressChanged(routeProgress)
mapControl.viewportDataSource.evaluate()
// draw the upcoming maneuver arrow on the map
val style = mapControl.ui.mapView.mapboxMap.style
if (style != null) {
val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
routeArrowView.renderManeuverUpdate(style, maneuverArrowResult)
}
// update top banner with maneuver instructions
val maneuvers = maneuverApi.getManeuvers(routeProgress)
maneuvers.fold(
{ error ->
Toast.makeText(
mapControl.context,
error.errorMessage,
Toast.LENGTH_SHORT
).show()
},
{
mapControl.ui.maneuverView.visibility = View.VISIBLE
mapControl.ui.maneuverView.renderManeuvers(maneuvers)
Garmin.instance.process(it[0])
}
)
// update bottom trip progress summary and send to HUD
tripProgressApi.getTripProgress(routeProgress).let {
mapControl.ui.tripProgressView.render(it)
Garmin.instance.process(it)
}
}
/**
* Gets notified whenever the tracked routes change.
*
* A change can mean:
* - routes get changed with [MapboxNavigation.setNavigationRoutes]
* - routes annotations get refreshed (for example, congestion annotation that indicate the live traffic along the route)
* - driver got off route and a reroute was executed
*/
val routesObserver = RoutesObserver { routeUpdateResult ->
if (routeUpdateResult.navigationRoutes.isNotEmpty()) {
// generate route geometries asynchronously and render them
routeLineApi.setNavigationRoutes(
routeUpdateResult.navigationRoutes
) { value ->
mapControl.ui.mapView.mapboxMap.style?.apply {
routeLineView.renderRouteDrawData(this, value)
}
}
// update the camera position to account for the new route
mapControl.viewportDataSource.onRouteChanged(routeUpdateResult.navigationRoutes.first())
mapControl.viewportDataSource.evaluate()
} else {
// remove the route line and route arrow from the map
val style = mapControl.ui.mapView.mapboxMap.style
if (style != null) {
routeLineApi.clearRouteLine { value ->
routeLineView.renderClearRouteLineValue(
style,
value
)
}
routeArrowView.render(style, routeArrowApi.clearArrows())
}
// remove the route reference from camera position evaluations
mapControl.viewportDataSource.clearRouteData()
mapControl.viewportDataSource.evaluate()
}
}
private fun findRoute(destination: Point) {
val originLocation = mapControl.navigationLocationProvider.lastLocation ?: return
val originPoint = Point.fromLngLat(originLocation.longitude, originLocation.latitude)
// execute a route request
// it's recommended to use the
// applyDefaultNavigationOptions and applyLanguageAndVoiceUnitOptions
// that make sure the route request is optimized
// to allow for support of all of the Navigation SDK features
mapControl.mapboxNavigation().requestRoutes(
RouteOptions.builder()
.applyDefaultNavigationOptions()
.applyLanguageAndVoiceUnitOptions(context)
.coordinatesList(listOf(originPoint, destination))
.apply {
// provide the bearing for the origin of the request to ensure
// that the returned route faces in the direction of the current user movement
originLocation.bearing?.let { bearing ->
bearingsList(
listOf(
Bearing.builder()
.angle(bearing)
.degrees(45.0)
.build(),
null
)
)
}
}
.layersList(listOf(mapControl.mapboxNavigation().getZLevel(), null))
.build(),
object : NavigationRouterCallback {
override fun onCanceled(routeOptions: RouteOptions, routerOrigin: String) {
// no impl
}
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
// no impl
}
override fun onRoutesReady(
routes: List<NavigationRoute>,
routerOrigin: String
) {
setRouteAndStartNavigation(routes)
}
}
)
}
private fun setRouteAndStartNavigation(routes: List<NavigationRoute>) {
// set routes, where the first route in the list is the primary route that
// will be used for active guidance
mapControl.mapboxNavigation().setNavigationRoutes(routes)
// show UI elements
mapControl.ui.soundButton.visibility = View.VISIBLE
mapControl.ui.routeOverview.visibility = View.VISIBLE
mapControl.ui.tripProgressCard.visibility = View.VISIBLE
// move the camera to overview when new route is available
mapControl.navigationCamera.requestNavigationCameraToOverview()
// start simulation
mapControl.replay.startSimulation(routes.first().directionsRoute)
}
private fun clearRouteAndStopNavigation() {
// clear
mapControl.mapboxNavigation().setNavigationRoutes(listOf())
// stop simulation
mapControl.replay.stopSimulation()
// hide UI elements
mapControl.ui.soundButton.visibility = View.INVISIBLE
mapControl.ui.maneuverView.visibility = View.INVISIBLE
mapControl.ui.routeOverview.visibility = View.INVISIBLE
mapControl.ui.tripProgressCard.visibility = View.INVISIBLE
}
fun cancel() {
maneuverApi.cancel()
routeLineApi.cancel()
routeLineView.cancel()
}
}

View file

@ -0,0 +1,111 @@
package eu.ztsh.garmin.mapbox
import android.content.Context
import com.mapbox.bindgen.Expected
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer
import com.mapbox.navigation.voice.api.MapboxSpeechApi
import com.mapbox.navigation.voice.api.MapboxVoiceInstructionsPlayer
import com.mapbox.navigation.voice.model.SpeechAnnouncement
import com.mapbox.navigation.voice.model.SpeechError
import com.mapbox.navigation.voice.model.SpeechValue
import com.mapbox.navigation.voice.model.SpeechVolume
import eu.ztsh.garmin.UI
import java.util.*
class VoiceControl(private val ui: UI, context: Context) {
/**
* Extracts message that should be communicated to the driver about the upcoming maneuver.
* When possible, downloads a synthesized audio file that can be played back to the driver.
*/
private lateinit var speechApi: MapboxSpeechApi
/**
* Plays the synthesized audio files with upcoming maneuver instructions
* or uses an on-device Text-To-Speech engine to communicate the message to the driver.
* NOTE: do not use lazy initialization for this class since it takes some time to initialize
* the system services required for on-device speech synthesis. With lazy initialization
* there is a high risk that said services will not be available when the first instruction
* has to be played. [MapboxVoiceInstructionsPlayer] should be instantiated in
* `Activity#onCreate`.
*/
private lateinit var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer
init {
speechApi = MapboxSpeechApi(
context,
Locale("pl").language
)
voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer(
context,
Locale("pl").language
)
ui.soundButton.setOnClickListener {
// mute/unmute voice instructions
isVoiceInstructionsMuted = !isVoiceInstructionsMuted
}
}
/**
* Stores and updates the state of whether the voice instructions should be played as they come or muted.
*/
private var isVoiceInstructionsMuted = false
set(value) {
field = value
if (value) {
ui.soundButton.muteAndExtend(UI.BUTTON_ANIMATION_DURATION)
voiceInstructionsPlayer.volume(SpeechVolume(0f))
} else {
ui.soundButton.unmuteAndExtend(UI.BUTTON_ANIMATION_DURATION)
voiceInstructionsPlayer.volume(SpeechVolume(1f))
}
}
/**
* Observes when a new voice instruction should be played.
*/
val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions ->
speechApi.generate(voiceInstructions, speechCallback)
}
/**
* Based on whether the synthesized audio file is available, the callback plays the file
* or uses the fall back which is played back using the on-device Text-To-Speech engine.
*/
private val speechCallback =
MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>> { expected ->
expected.fold(
{ error ->
// play the instruction via fallback text-to-speech engine
voiceInstructionsPlayer.play(
error.fallback,
voiceInstructionsPlayerCallback
)
},
{ value ->
// play the sound file from the external generator
voiceInstructionsPlayer.play(
value.announcement,
voiceInstructionsPlayerCallback
)
}
)
}
/**
* When a synthesized audio file was downloaded, this callback cleans up the disk after it was played.
*/
private val voiceInstructionsPlayerCallback =
MapboxNavigationConsumer<SpeechAnnouncement> { value ->
// remove already consumed file to free-up space
speechApi.clean(value)
}
fun cancel() {
speechApi.cancel()
voiceInstructionsPlayer.shutdown()
}
}

View file

@ -0,0 +1,73 @@
package eu.ztsh.garmin.mock
import com.mapbox.api.directions.v5.models.DirectionsRoute
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.replay.route.ReplayProgressObserver
import com.mapbox.navigation.core.replay.route.ReplayRouteMapper
import eu.ztsh.garmin.mapbox.MapControl
import java.util.*
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class ReplayResources(private val mapControl: MapControl) {
/**
* Debug observer that makes sure the replayer has always an up-to-date information to generate mock updates.
*/
private lateinit var replayProgressObserver: ReplayProgressObserver
/**
* Debug object that converts a route into events that can be replayed to navigate a route.
*/
private val replayRouteMapper = ReplayRouteMapper()
fun replayOriginLocation() {
with(mapControl.mapboxNavigation().mapboxReplayer) {
play()
pushEvents(
listOf(
ReplayRouteMapper.mapToUpdateLocation(
Date().time.toDouble(),
com.mapbox.geojson.Point.fromLngLat(18.531478, 50.088155)
)
)
)
playFirstLocation()
}
}
fun startSimulation(route: DirectionsRoute) {
with(mapControl.mapboxNavigation()) {
mapboxReplayer.stop()
mapboxReplayer.clearEvents()
val replayData = replayRouteMapper.mapDirectionsRouteGeometry(route)
mapboxReplayer.pushEvents(replayData)
mapboxReplayer.seekTo(replayData[0])
mapboxReplayer.play()
}
}
fun stopSimulation() {
with(mapControl.mapboxNavigation()) {
mapboxReplayer.stop()
mapboxReplayer.clearEvents()
}
}
fun onAttached(mapboxNavigation: MapboxNavigation) {
replayProgressObserver = ReplayProgressObserver(mapboxNavigation.mapboxReplayer)
mapboxNavigation.registerRouteProgressObserver(replayProgressObserver)
// Start the trip session to being receiving location updates in free drive
// and later when a route is set also receiving route progress updates.
// In case of `startReplayTripSession`,
// location events are emitted by the `MapboxReplayer`
mapboxNavigation.startReplayTripSession()
}
fun onDetached(mapboxNavigation: MapboxNavigation) {
mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver)
mapboxNavigation.mapboxReplayer.finish()
}
}

View file

@ -0,0 +1,50 @@
package eu.ztsh.garmin.util
import android.app.Activity
import android.widget.Toast
import com.mapbox.android.core.permissions.PermissionsListener
import com.mapbox.android.core.permissions.PermissionsManager
import java.lang.ref.WeakReference
class PermissionsHelper(val activityRef: WeakReference<Activity>) {
private lateinit var permissionsManager: PermissionsManager
fun checkPermissions(onMapReady: () -> Unit) {
activityRef.get()?.let { activity: Activity ->
if (PermissionsManager.areLocationPermissionsGranted(activity)) {
onMapReady()
} else {
permissionsManager = PermissionsManager(object : PermissionsListener {
override fun onExplanationNeeded(permissionsToExplain: List<String>) {
activityRef.get()?.let {
Toast.makeText(
it, "You need to accept location permissions.",
Toast.LENGTH_SHORT
).show()
}
}
override fun onPermissionResult(granted: Boolean) {
activityRef.get()?.let {
if (granted) {
onMapReady()
} else {
it.finish()
}
}
}
})
permissionsManager.requestLocationPermissions(activity)
}
}
}
fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}

View file

@ -1,9 +1,80 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.mapbox.navigation.dropin.NavigationView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/navigationView" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
app:accessToken="@string/mapbox_access_token"
xmlns:app="http://schemas.android.com/apk/res-auto">
</com.mapbox.navigation.dropin.NavigationView> <com.mapbox.maps.MapView
android:id="@+id/mapView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/tripProgressCard"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:cardElevation="8dp"
app:cardUseCompatPadding="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView
android:id="@+id/tripProgressView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/stop"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="12dp"
app:srcCompat="@android:drawable/ic_delete" />
</androidx.cardview.widget.CardView>
<com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
android:id="@+id/maneuverView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.mapbox.navigation.ui.components.voice.view.MapboxSoundButton
android:id="@+id/soundButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/maneuverView" />
<com.mapbox.navigation.ui.components.maps.camera.view.MapboxRouteOverviewButton
android:id="@+id/routeOverview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/soundButton" />
<com.mapbox.navigation.ui.components.maps.camera.view.MapboxRecenterButton
android:id="@+id/recenter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/routeOverview" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -9,8 +9,8 @@ class GarminMapperTest {
@Test @Test
fun linesTest() { fun linesTest() {
linesTest( linesTest(
listOf(Lane.DotsLeft), setOf(Lane.DotsLeft),
listOf(), setOf(),
intArrayOf(2, 128, 0), intArrayOf(2, 128, 0),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 128, 0, 141, 16, 3) intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 128, 0, 141, 16, 3)
) )
@ -51,14 +51,14 @@ class GarminMapperTest {
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 64, 2, 203, 16, 3) intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 64, 2, 203, 16, 3)
) )
linesTest( linesTest(
listOf(Lane.DotsRight), setOf(Lane.DotsRight),
listOf(Lane.OuterRight, Lane.MiddleRight, Lane.InnerRight, Lane.InnerLeft, Lane.MiddleLeft, Lane.OuterLeft), setOf(Lane.OuterRight, Lane.MiddleRight, Lane.InnerRight, Lane.InnerLeft, Lane.MiddleLeft, Lane.OuterLeft),
intArrayOf(2, 1, 126), intArrayOf(2, 1, 126),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 1, 126, 142, 16, 3) intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 1, 126, 142, 16, 3)
) )
linesTest( linesTest(
listOf(), setOf(),
listOf(), setOf(),
intArrayOf(2, 0, 0), intArrayOf(2, 0, 0),
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 0, 0, 13, 16, 3) intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 0, 0, 13, 16, 3)
) )
@ -249,14 +249,12 @@ class GarminMapperTest {
} }
private fun linesTest(outlines: Lane, arrows: Lane, expectedRaw: IntArray, expectedBoxed: IntArray) { private fun linesTest(outlines: Lane, arrows: Lane, expectedRaw: IntArray, expectedBoxed: IntArray) {
linesTest(listOf(outlines), listOf(arrows), expectedRaw, expectedBoxed) linesTest(setOf(outlines), setOf(arrows), expectedRaw, expectedBoxed)
} }
private fun linesTest(outlines: List<Lane>, arrows: List<Lane>, expectedRaw: IntArray, expectedBoxed: IntArray) { private fun linesTest(outlines: Set<Lane>, arrows: Set<Lane>, expectedRaw: IntArray, expectedBoxed: IntArray) {
val state = State() val lanes = Lanes(Arrows(arrows), Arrows(outlines))
state.lineOutlines = outlines makeAssertions(GarminMapper.map(lanes), expectedRaw, expectedBoxed)
state.lineArrows = arrows
makeAssertions(GarminMapper.setLines(state), expectedRaw, expectedBoxed)
} }
private fun directionTest(outAngle: OutAngle, expectedRaw: IntArray, expectedBoxed: IntArray) { private fun directionTest(outAngle: OutAngle, expectedRaw: IntArray, expectedBoxed: IntArray) {
@ -264,10 +262,8 @@ class GarminMapperTest {
} }
private fun directionTest(outAngle: OutAngle, outType: OutType, expectedRaw: IntArray, expectedBoxed: IntArray) { private fun directionTest(outAngle: OutAngle, outType: OutType, expectedRaw: IntArray, expectedBoxed: IntArray) {
val state = State() val direction = Direction(outAngle, outType)
state.direction.angle = outAngle makeAssertions(GarminMapper.map(direction), expectedRaw, expectedBoxed)
state.direction.out = outType
makeAssertions(GarminMapper.setDirection(state), expectedRaw, expectedBoxed)
} }
private fun makeAssertions(resultRaw: IntArray, expectedRaw: IntArray, expectedBoxed: IntArray) { private fun makeAssertions(resultRaw: IntArray, expectedRaw: IntArray, expectedBoxed: IntArray) {

View file

@ -5,7 +5,7 @@ buildscript {
} }
} }
plugins { plugins {
id 'com.android.application' version '7.4.1' apply false id 'com.android.application' version '8.2.0' apply false
id 'com.android.library' version '7.4.1' apply false id 'com.android.library' version '8.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.24' apply false id 'org.jetbrains.kotlin.android' version '1.9.24' apply false
} }

View file

@ -1,6 +1,6 @@
#Thu Jun 22 21:00:17 CEST 2023 #Thu Jun 22 21:00:17 CEST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

@ -63,7 +63,8 @@ class Controller:
def clear(self): def clear(self):
self.send_hud([0x03, 0, 0, 0, 0x00, 0, 0]) self.send_hud([0x03, 0, 0, 0, 0x00, 0, 0])
def _as_dgt(self, n: int) -> int: def _as_dgt(self, number: float) -> int:
n = int(number)
if n == 0: if n == 0:
return 0 return 0
n %= 10 n %= 10
@ -89,9 +90,17 @@ class Controller:
arr = [0x01, param_1, param_2, param_3] arr = [0x01, param_1, param_2, param_3]
self.send_hud(arr) self.send_hud(arr)
def set_distance(self, distance, unit: Unit = Unit.Any): def set_distance(self, distance: float, unit: Unit = Unit.Any):
arr = [0x03, self._as_dgt(distance // 1000), self._as_dgt(distance // 100), self._as_dgt(distance // 10), 0x00, is_float = int(distance * 10) == int(distance) * 10
self._as_dgt(distance), unit.value] if not is_float:
distance = distance * 10
arr = [0x03,
self._as_dgt(distance // 1000),
self._as_dgt(distance // 100),
self._as_dgt(distance // 10),
0x00 if is_float else 0xff,
self._as_dgt(distance),
unit.value]
self.send_hud(arr) self.send_hud(arr)
def set_speed(self, speed: int, limit: int = 0, acc: bool = False): def set_speed(self, speed: int, limit: int = 0, acc: bool = False):

View file

@ -1,4 +1,4 @@
from main import * from main import Controller, Lane, OutType, OutAngle, Unit
from time import sleep from time import sleep
interval = 0.2 interval = 0.2
@ -32,7 +32,6 @@ def lines(controller: Controller):
def direction(controller: Controller): def direction(controller: Controller):
print("Direction") print("Direction")
controller.set_direction(OutAngle.RightDown)
controller.set_direction(OutAngle.SharpRight) controller.set_direction(OutAngle.SharpRight)
controller.set_direction(OutAngle.Right) controller.set_direction(OutAngle.Right)
controller.set_direction(OutAngle.EasyRight) controller.set_direction(OutAngle.EasyRight)
@ -41,6 +40,7 @@ def direction(controller: Controller):
controller.set_direction(OutAngle.Left) controller.set_direction(OutAngle.Left)
controller.set_direction(OutAngle.SharpLeft) controller.set_direction(OutAngle.SharpLeft)
controller.set_direction(OutAngle.LeftDown) controller.set_direction(OutAngle.LeftDown)
controller.set_direction(OutAngle.RightDown)
controller.set_direction(OutAngle.Down) controller.set_direction(OutAngle.Down)
controller.set_direction(OutAngle.SharpRight, OutType.LongerLane) controller.set_direction(OutAngle.SharpRight, OutType.LongerLane)
controller.set_direction(OutAngle.Right, OutType.LongerLane) controller.set_direction(OutAngle.Right, OutType.LongerLane)
@ -49,6 +49,12 @@ def direction(controller: Controller):
controller.set_direction(OutAngle.EasyLeft, OutType.LongerLane) controller.set_direction(OutAngle.EasyLeft, OutType.LongerLane)
controller.set_direction(OutAngle.Left, OutType.LongerLane) controller.set_direction(OutAngle.Left, OutType.LongerLane)
controller.set_direction(OutAngle.SharpLeft, OutType.LongerLane) controller.set_direction(OutAngle.SharpLeft, OutType.LongerLane)
roundabout(controller)
controller.set_direction(OutAngle.Left, OutType.Flag)
controller.set_direction(OutAngle.Right, OutType.Flag)
def roundabout(controller: Controller):
controller.set_direction(OutAngle.SharpRight, OutType.RightRoundabout) controller.set_direction(OutAngle.SharpRight, OutType.RightRoundabout)
controller.set_direction(OutAngle.Right, OutType.RightRoundabout) controller.set_direction(OutAngle.Right, OutType.RightRoundabout)
controller.set_direction(OutAngle.EasyRight, OutType.RightRoundabout) controller.set_direction(OutAngle.EasyRight, OutType.RightRoundabout)
@ -56,18 +62,18 @@ def direction(controller: Controller):
controller.set_direction(OutAngle.EasyLeft, OutType.RightRoundabout) controller.set_direction(OutAngle.EasyLeft, OutType.RightRoundabout)
controller.set_direction(OutAngle.Left, OutType.RightRoundabout) controller.set_direction(OutAngle.Left, OutType.RightRoundabout)
controller.set_direction(OutAngle.SharpLeft, OutType.RightRoundabout) controller.set_direction(OutAngle.SharpLeft, OutType.RightRoundabout)
controller.set_direction(OutAngle.Left, OutType.Flag) controller.set_direction(OutAngle.Down, OutType.RightRoundabout)
controller.set_direction(OutAngle.Right, OutType.Flag)
def distance(controller: Controller): def distance(controller: Controller):
print("Distance") print("Distance")
controller.set_distance(999) controller.set_distance(555.5)
controller.set_distance(999, Unit.Kilometres) controller.set_distance(6666)
controller.set_distance(777.7)
controller.set_distance(888, Unit.Kilometres)
controller.set_distance(999, Unit.Metres) controller.set_distance(999, Unit.Metres)
controller.set_distance(999, Unit.Foot) controller.set_distance(999, Unit.Foot)
controller.set_distance(999, Unit.Miles) controller.set_distance(999, Unit.Miles)
pass
def speed(controller: Controller): def speed(controller: Controller):
@ -77,7 +83,6 @@ def speed(controller: Controller):
controller.set_speed(50, 100) controller.set_speed(50, 100)
controller.set_speed(150, 100) controller.set_speed(150, 100)
controller.set_speed(50, 100, True) controller.set_speed(50, 100, True)
pass
def time(controller: Controller): def time(controller: Controller):
@ -85,7 +90,6 @@ def time(controller: Controller):
controller.set_time(22, 22) controller.set_time(22, 22)
controller.set_time(22, 22, traffic=True) controller.set_time(22, 22, traffic=True)
controller.set_time(22, 22, flag=True) controller.set_time(22, 22, flag=True)
pass
def control(controller: Controller): def control(controller: Controller):
@ -104,8 +108,34 @@ def compass(controller: Controller):
controller.set_compass(247.5) controller.set_compass(247.5)
controller.set_compass(292.5) controller.set_compass(292.5)
controller.set_compass(337.5) controller.set_compass(337.5)
pass
def route(controller: Controller):
print("Route")
controller.set_direction(OutAngle.Left)
controller.set_gps(True)
controller.set_speed(50, 50)
controller.set_distance(1.2, Unit.Kilometres)
sleep(1)
controller.set_distance(1.1, Unit.Kilometres)
sleep(1)
controller.set_distance(1, Unit.Kilometres)
sleep(1)
remaining = 900
while remaining > 0:
controller.set_distance(remaining, Unit.Metres)
sleep(1)
remaining -= 100
controller.set_direction(OutAngle.Right, OutType.Flag)
remaining = 900
while remaining > 0:
controller.set_distance(remaining, Unit.Metres)
sleep(1)
remaining -= 100
if __name__ == '__main__': if __name__ == '__main__':
instance = Controller('/dev/rfcomm0') import os
name = '/dev/rfcomm0' if os.name != 'nt' else 'COM8'
instance = Controller(name)
suite(instance)