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:
commit
06a9f12446
22 changed files with 1343 additions and 389 deletions
|
@ -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'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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) }
|
|
||||||
|
|
||||||
}
|
|
59
app/src/main/java/eu/ztsh/garmin/UI.kt
Normal file
59
app/src/main/java/eu/ztsh/garmin/UI.kt
Normal 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
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
49
app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt
Normal file
49
app/src/main/java/eu/ztsh/garmin/mapbox/LocationObserver.kt
Normal 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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
158
app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt
Normal file
158
app/src/main/java/eu/ztsh/garmin/mapbox/MapControl.kt
Normal 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"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
309
app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt
Normal file
309
app/src/main/java/eu/ztsh/garmin/mapbox/RouteControl.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
111
app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt
Normal file
111
app/src/main/java/eu/ztsh/garmin/mapbox/VoiceControl.kt
Normal 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()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
73
app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt
Normal file
73
app/src/main/java/eu/ztsh/garmin/mock/ReplayResources.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
50
app/src/main/java/eu/ztsh/garmin/util/PermissionsHelper.kt
Normal file
50
app/src/main/java/eu/ztsh/garmin/util/PermissionsHelper.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue