Merge branch 'mapbox'
This commit is contained in:
commit
82a29cb00a
30 changed files with 2384 additions and 580 deletions
|
@ -6,16 +6,18 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace 'eu.ztsh.garmin'
|
||||
compileSdk 31
|
||||
compileSdk 34
|
||||
ndkVersion "23.2.8568313"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "eu.ztsh.garmin"
|
||||
minSdk 29
|
||||
targetSdk 31
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
buildConfigField "String", "MAPBOX_DOWNLOADS_TOKEN", "\"$MAPBOX_DOWNLOADS_TOKEN\""
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -33,27 +35,26 @@ android {
|
|||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
ext {
|
||||
mapboxVersion = '3.2.0'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Notification parser
|
||||
implementation 'com.github.3v1n0.GMapsParser:navparser:0.2.1'
|
||||
//
|
||||
// // Bluetooth serial
|
||||
// implementation 'com.github.harry1453:android-bluetooth-serial:v1.1'
|
||||
// implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
|
||||
// implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.4'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
// testImplementation 'junit:junit:4.13.2'
|
||||
implementation "com.mapbox.navigationcore:navigation:$mapboxVersion"
|
||||
implementation "com.mapbox.navigationcore:ui-maps:$mapboxVersion"
|
||||
implementation "com.mapbox.navigationcore:voice:$mapboxVersion"
|
||||
implementation "com.mapbox.navigationcore:tripdata:$mapboxVersion"
|
||||
implementation "com.mapbox.navigationcore:ui-components:$mapboxVersion"
|
||||
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'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
testImplementation 'org.assertj:assertj-core:3.24.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
|
||||
}
|
||||
|
|
|
@ -8,21 +8,15 @@
|
|||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||
android:maxSdkVersion="30" />
|
||||
|
||||
<!-- Needed only if your app looks for Bluetooth devices.
|
||||
If your app doesn't use Bluetooth scan results to derive physical
|
||||
location information, you can strongly assert that your app
|
||||
doesn't derive physical location. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
|
||||
<!-- Needed only if your app communicates with already-paired Bluetooth
|
||||
devices. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
|
||||
|
||||
<queries>
|
||||
<package android:name="com.google.android.apps.maps" />
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
|
@ -32,15 +26,6 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Garmin"
|
||||
tools:targetApi="31">
|
||||
<service
|
||||
android:name="me.trevi.navparser.service.NavigationListenerEmitter"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
|
|
|
@ -5,12 +5,19 @@ import android.bluetooth.BluetoothAdapter
|
|||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.util.Log
|
||||
import me.trevi.navparser.lib.NavigationData
|
||||
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
|
||||
import com.mapbox.navigation.tripdata.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.GarminMapper
|
||||
import eu.ztsh.garmin.data.Lanes
|
||||
import eu.ztsh.garmin.data.MapboxMapper
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.SynchronousQueue
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class Garmin(
|
||||
val context: MainActivity,
|
||||
|
@ -18,48 +25,202 @@ class Garmin(
|
|||
val adapter: BluetoothAdapter
|
||||
) {
|
||||
|
||||
private lateinit var thread: ConnectThread
|
||||
private lateinit var connection: ConnectThread
|
||||
private lateinit var maneuvers: ManeuverProcessingThread
|
||||
private lateinit var trips: TripProgressProcessingThread
|
||||
private lateinit var locations: LocationMatcherProcessingThread
|
||||
private val cache = DataCache()
|
||||
|
||||
private val processingPool = Executors.newFixedThreadPool(8)
|
||||
|
||||
fun start() {
|
||||
thread = ConnectThread()
|
||||
thread.start()
|
||||
}
|
||||
connection = ConnectThread()
|
||||
connection.start()
|
||||
|
||||
fun send(data: IntArray) {
|
||||
thread.enqueue(data)
|
||||
}
|
||||
maneuvers = ManeuverProcessingThread()
|
||||
maneuvers.start()
|
||||
|
||||
fun send(data: NavigationData) {
|
||||
Log.d("SEND_DATA", "$data.remainingDistance.distance")
|
||||
trips = TripProgressProcessingThread()
|
||||
trips.start()
|
||||
|
||||
locations = LocationMatcherProcessingThread()
|
||||
locations.start()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
thread.close()
|
||||
connection.close()
|
||||
|
||||
maneuvers.interrupt()
|
||||
maneuvers.join(0)
|
||||
|
||||
trips.interrupt()
|
||||
trips.join(0)
|
||||
|
||||
locations.interrupt()
|
||||
locations.join(0)
|
||||
|
||||
processingPool.shutdown()
|
||||
}
|
||||
|
||||
fun process(maneuver: Maneuver) {
|
||||
processingPool.submit{maneuvers.enqueue(maneuver)}
|
||||
}
|
||||
|
||||
fun process(tripProgressUpdateValue: TripProgressUpdateValue) {
|
||||
processingPool.submit{trips.enqueue(tripProgressUpdateValue)}
|
||||
}
|
||||
|
||||
fun process(locationMatcherResult: LocationMatcherResult) {
|
||||
processingPool.submit{locations.enqueue(locationMatcherResult)}
|
||||
}
|
||||
|
||||
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() {
|
||||
while (!stop) {
|
||||
val maybeItem = queue.poll()
|
||||
val item = mapAndSend(maybeItem)
|
||||
if (item != null) {
|
||||
Log.d(TAG, "run: Cache updated")
|
||||
updateCache(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun interrupt() {
|
||||
stop = true
|
||||
super.interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ConnectThread : Thread() {
|
||||
|
||||
private val queue: SynchronousQueue<IntArray> = SynchronousQueue()
|
||||
private var current: IntArray = intArrayOf()
|
||||
|
||||
private val socket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
|
||||
context.checkBt()
|
||||
device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
|
||||
}
|
||||
|
||||
private val queue: SynchronousQueue<IntArray> = SynchronousQueue()
|
||||
|
||||
fun enqueue(data: IntArray) {
|
||||
// queue.clear() // for immediate run
|
||||
queue.put(data)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
// Cancel discovery because it otherwise slows down the connection.
|
||||
context.checkBt()
|
||||
adapter.cancelDiscovery()
|
||||
socket?.connect()
|
||||
sleep(3000)
|
||||
readAll()
|
||||
while (true) {
|
||||
queue.poll()?.let { send(it) }
|
||||
try {
|
||||
socket?.connect()
|
||||
context.setConnectionStatus(true)
|
||||
sleep(3000)
|
||||
readAll()
|
||||
send(intArrayOf(0x07, 0x01)) // Set GPS to true
|
||||
while (true) {
|
||||
val newCurrent = queue.poll()
|
||||
if (newCurrent == null) {
|
||||
Log.d(TAG, "run (${currentThread().name}): Sleep...")
|
||||
sleep(250)
|
||||
} else {
|
||||
current = newCurrent
|
||||
}
|
||||
send(current)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.d(TAG, "Not connected", e)
|
||||
context.setConnectionStatus(false)
|
||||
while (true) {
|
||||
// Just dequeue
|
||||
// TODO: Add option to reconnect
|
||||
queue.poll()
|
||||
sleep(900)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +233,15 @@ class Garmin(
|
|||
}
|
||||
}
|
||||
|
||||
fun readAll() {
|
||||
fun enqueue(data: IntArray) {
|
||||
queue.put(data)
|
||||
}
|
||||
|
||||
private fun send(data: IntArray) {
|
||||
sendRaw(prepareData(data))
|
||||
}
|
||||
|
||||
private fun readAll() {
|
||||
val buffer = ByteArray(64)
|
||||
val istr = socket!!.inputStream
|
||||
istr!!.let {
|
||||
|
@ -82,20 +251,18 @@ class Garmin(
|
|||
}
|
||||
}
|
||||
|
||||
fun send(data: IntArray) {
|
||||
sendRaw(prepareData(data))
|
||||
}
|
||||
|
||||
private fun sendRaw(buff: IntArray) {
|
||||
buff.forEach { socket!!.outputStream.write(it) }
|
||||
socket!!.outputStream.flush()
|
||||
sleep(2000)
|
||||
readAll()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
lateinit var instance: Garmin
|
||||
|
||||
fun prepareData(input: IntArray): IntArray {
|
||||
val n = input.size
|
||||
var crc = (0xeb + n + n).toUInt()
|
||||
|
|
|
@ -2,94 +2,79 @@ package eu.ztsh.garmin
|
|||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.CompoundButton
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import com.mapbox.navigation.base.options.NavigationOptions
|
||||
import com.mapbox.navigation.core.MapboxNavigation
|
||||
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
|
||||
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
|
||||
import com.mapbox.navigation.core.lifecycle.requireMapboxNavigation
|
||||
import eu.ztsh.garmin.databinding.ActivityMainBinding
|
||||
import me.trevi.navparser.lib.NavigationData
|
||||
import me.trevi.navparser.service.*
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import eu.ztsh.garmin.mapbox.MapControl
|
||||
import eu.ztsh.garmin.util.PermissionsHelper
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
var navigating: Boolean = false
|
||||
lateinit var garmin: Garmin
|
||||
private lateinit var binding : ActivityMainBinding
|
||||
private val navDataModel: NavigationDataModel by viewModels()
|
||||
// lateinit var garmin: Garmin
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
binding.mapView
|
||||
permissionsHelper.checkPermissions {
|
||||
mapControl = MapControl(this, UI(binding), resources)
|
||||
mapControl.init()
|
||||
|
||||
MapboxNavigationApp.setup(
|
||||
NavigationOptions.Builder(applicationContext)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
bluetoothInit()
|
||||
|
||||
binding.enabledSwitch.setOnCheckedChangeListener {
|
||||
_, isChecked ->
|
||||
run {
|
||||
navigating = isChecked
|
||||
|
||||
setNavigationData(NavigationData(true))
|
||||
navDataModel.liveData.observe(this) { setNavigationData(it) }
|
||||
Log.d(TAG, "OK")
|
||||
}
|
||||
}
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
startServiceListener()
|
||||
checkNotificationsAccess()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, intent)
|
||||
|
||||
Log.d(TAG, "Got activity result: $intent, ${intent?.extras}, ${intent?.action}")
|
||||
when (intent?.action) {
|
||||
|
||||
NOTIFICATIONS_ACCESS_RESULT -> {
|
||||
intent.getBooleanExtra(NOTIFICATIONS_ACCESS_RESULT, false).also {
|
||||
val notificationAccess = it
|
||||
|
||||
if (!notificationAccess) {
|
||||
Log.e(TAG, "No notification access for ${NavigationListenerEmitter::class.qualifiedName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NAVIGATION_DATA_UPDATED -> {
|
||||
// gotoFragment(R.id.NavigationFragment)
|
||||
|
||||
val navData = intent.getParcelableExtra<NavigationData>(NAVIGATION_DATA)
|
||||
|
||||
Log.d(TAG, "Got navigation data $navData")
|
||||
navDataModel.data = navData
|
||||
}
|
||||
|
||||
NAVIGATION_STARTED -> {
|
||||
Log.d(TAG, "Started")
|
||||
}
|
||||
|
||||
NAVIGATION_STOPPED -> {
|
||||
Log.d(TAG, "Stopped")
|
||||
}
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
mapControl.onDestroy()
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
|
||||
private fun bluetoothInit() {
|
||||
|
@ -108,10 +93,10 @@ class MainActivity : AppCompatActivity() {
|
|||
val context = this
|
||||
pairedDevices?.firstOrNull { device ->
|
||||
Log.d(TAG, device.name)
|
||||
device.name.equals("GARMIN HUD")
|
||||
device.name == "GARMIN HUD"
|
||||
}?.apply {
|
||||
garmin = Garmin(context, this, bluetoothAdapter)
|
||||
garmin.start()
|
||||
Garmin.instance = Garmin(context, this, bluetoothAdapter)
|
||||
Garmin.instance.start()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -138,33 +123,19 @@ class MainActivity : AppCompatActivity() {
|
|||
return true
|
||||
}
|
||||
|
||||
private fun startServiceListener() {
|
||||
Log.d(TAG, "Start Service Listener")
|
||||
setServiceListenerIntent(createPendingResult(100, Intent(), 0))
|
||||
}
|
||||
|
||||
fun stopServiceListener() {
|
||||
Log.d(TAG, "Stopping Service Listener")
|
||||
setServiceListenerIntent(null)
|
||||
}
|
||||
|
||||
private fun setServiceListenerIntent(pendingIntent: PendingIntent?) {
|
||||
startService(serviceIntent(SET_INTENT).putExtra(PENDING_INTENT, pendingIntent))
|
||||
}
|
||||
|
||||
private fun checkNotificationsAccess() {
|
||||
startService(serviceIntent(CHECK_NOTIFICATIONS_ACCESS))
|
||||
}
|
||||
|
||||
private fun serviceIntent(action: String): Intent {
|
||||
return Intent(applicationContext, NavigationListenerEmitter::class.java).setAction(action)
|
||||
}
|
||||
private fun setNavigationData(navData: NavigationData) {
|
||||
Log.d(TAG, "Sending $navData")
|
||||
garmin.send(navData)
|
||||
fun setConnectionStatus(success: Boolean) {
|
||||
this.runOnUiThread {
|
||||
if (success) {
|
||||
Toast.makeText(this, "Garmin connected", Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
// TODO: Make snackbar with reconnect option
|
||||
Toast.makeText(this, "Garmin not connected", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "bt"
|
||||
}
|
||||
|
||||
|
|
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
|
||||
|
||||
}
|
|
@ -1,310 +0,0 @@
|
|||
package eu.ztsh.garmin.bt
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.AsyncTask
|
||||
import android.util.Log
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import eu.ztsh.garmin.MainActivity
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
|
||||
class BluetoothSerial(var context: MainActivity, var messageHandler: MessageHandler, devicePrefix: String) {
|
||||
var connected = false
|
||||
var bluetoothDevice: BluetoothDevice? = null
|
||||
var serialSocket: BluetoothSocket? = null
|
||||
var serialInputStream: InputStream? = null
|
||||
var serialOutputStream: OutputStream? = null
|
||||
var serialReader: SerialReader? = null
|
||||
var connectionTask: AsyncTask<Void?, Void?, BluetoothDevice?>? = null
|
||||
var devicePrefix: String
|
||||
|
||||
/**
|
||||
* Listens for discount message from bluetooth system and restablishing a connection
|
||||
*/
|
||||
private val bluetoothReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action
|
||||
val eventDevice = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action) {
|
||||
if (bluetoothDevice != null && bluetoothDevice == eventDevice) {
|
||||
Log.i(BMX_BLUETOOTH, "Received bluetooth disconnect notice")
|
||||
|
||||
//clean up any streams
|
||||
close()
|
||||
|
||||
//reestablish connect
|
||||
connect()
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(BLUETOOTH_DISCONNECTED))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
this.devicePrefix = devicePrefix.uppercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
fun onPause() {
|
||||
context.unregisterReceiver(bluetoothReceiver)
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
//listen for bluetooth disconnect
|
||||
val disconnectIntent = IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||
context.registerReceiver(bluetoothReceiver, disconnectIntent)
|
||||
|
||||
//reestablishes a connection is one doesn't exist
|
||||
if (!connected) {
|
||||
connect()
|
||||
} else {
|
||||
val intent = Intent(BLUETOOTH_CONNECTED)
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the bluetooth serial connections, uses the LocalBroadcastManager when
|
||||
* connection is established
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
fun connect() {
|
||||
if (connected) {
|
||||
Log.e(BMX_BLUETOOTH, "Connection request while already connected")
|
||||
return
|
||||
}
|
||||
if (connectionTask != null && connectionTask!!.status == AsyncTask.Status.RUNNING) {
|
||||
Log.e(BMX_BLUETOOTH, "Connection request while attempting connection")
|
||||
return
|
||||
}
|
||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
||||
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
|
||||
return
|
||||
}
|
||||
context.checkBt()
|
||||
val pairedDevices: List<BluetoothDevice> = ArrayList(bluetoothAdapter.getBondedDevices())
|
||||
if (pairedDevices.isNotEmpty()) {
|
||||
bluetoothAdapter.cancelDiscovery()
|
||||
/**
|
||||
* AsyncTask to handle the establishing of a bluetooth connection
|
||||
*/
|
||||
// connectionTask = object : AsyncTask<Void?, Void?, BluetoothDevice?>() {
|
||||
// var MAX_ATTEMPTS = 30
|
||||
// var attemptCounter = 0
|
||||
// protected override fun doInBackground(vararg params: Void): BluetoothDevice? {
|
||||
// while (!isCancelled) { //need to kill without calling onCancel
|
||||
// for (device in pairedDevices) {
|
||||
// if (device.getName().uppercase(Locale.getDefault()).startsWith(devicePrefix)) {
|
||||
// Log.i(
|
||||
// BMX_BLUETOOTH,
|
||||
// attemptCounter.toString() + ": Attempting connection to " + device.getName()
|
||||
// )
|
||||
// try {
|
||||
// serialSocket = try {
|
||||
// // Standard SerialPortService ID
|
||||
// val uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
|
||||
// device.createRfcommSocketToServiceRecord(uuid)
|
||||
// } catch (ce: Exception) {
|
||||
// connectViaReflection(device)
|
||||
// }
|
||||
//
|
||||
// //setup the connect streams
|
||||
// serialSocket!!.connect()
|
||||
// serialInputStream = serialSocket!!.inputStream
|
||||
// serialOutputStream = serialSocket!!.outputStream
|
||||
// connected = true
|
||||
// Log.i(BMX_BLUETOOTH, "Connected to " + device.getName())
|
||||
// return device
|
||||
// } catch (e: Exception) {
|
||||
// serialSocket = null
|
||||
// serialInputStream = null
|
||||
// serialOutputStream = null
|
||||
// Log.i(BMX_BLUETOOTH, e.message!!)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// try {
|
||||
// attemptCounter++
|
||||
// if (attemptCounter > MAX_ATTEMPTS) cancel(false) else Thread.sleep(1000)
|
||||
// } catch (e: InterruptedException) {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// Log.i(BMX_BLUETOOTH, "Stopping connection attempts")
|
||||
// val intent = Intent(BLUETOOTH_FAILED)
|
||||
// LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
// override fun onPostExecute(result: BluetoothDevice?) {
|
||||
// super.onPostExecute(result)
|
||||
// bluetoothDevice = result
|
||||
//
|
||||
// //start thread responsible for reading from inputstream
|
||||
// serialReader = SerialReader()
|
||||
// serialReader!!.start()
|
||||
//
|
||||
// //send connection message
|
||||
// val intent = Intent(BLUETOOTH_CONNECTED)
|
||||
// LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||
// }
|
||||
// }
|
||||
// connectionTask.execute()
|
||||
}
|
||||
}
|
||||
|
||||
// see: http://stackoverflow.com/questions/3397071/service-discovery-failed-exception-using-bluetooth-on-android
|
||||
@Throws(Exception::class)
|
||||
private fun connectViaReflection(device: BluetoothDevice): BluetoothSocket {
|
||||
val m = device.javaClass.getMethod(
|
||||
"createRfcommSocket", *arrayOf<Class<*>?>(
|
||||
Int::class.javaPrimitiveType
|
||||
)
|
||||
)
|
||||
return m.invoke(device, 1) as BluetoothSocket
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun available(): Int {
|
||||
if (connected) return serialInputStream!!.available()
|
||||
throw RuntimeException("Connection lost, reconnecting now.")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun read(): Int {
|
||||
if (connected) return serialInputStream!!.read()
|
||||
throw RuntimeException("Connection lost, reconnecting now.")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun read(buffer: ByteArray?): Int {
|
||||
if (connected) return serialInputStream!!.read(buffer)
|
||||
throw RuntimeException("Connection lost, reconnecting now.")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun read(buffer: ByteArray?, byteOffset: Int, byteCount: Int): Int {
|
||||
if (connected) return serialInputStream!!.read(buffer, byteOffset, byteCount)
|
||||
throw RuntimeException("Connection lost, reconnecting now.")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(buffer: ByteArray?) {
|
||||
if (connected) serialOutputStream!!.write(buffer)
|
||||
throw RuntimeException("Connection lost, reconnecting now.")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(oneByte: Int) {
|
||||
if (connected) serialOutputStream!!.write(oneByte)
|
||||
throw RuntimeException("Connection lost, reconnecting now.")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun write(buffer: ByteArray?, offset: Int, count: Int) {
|
||||
serialOutputStream!!.write(buffer, offset, count)
|
||||
throw RuntimeException("Connection lost, reconnecting now.")
|
||||
}
|
||||
|
||||
inner class SerialReader : Thread() {
|
||||
var buffer = ByteArray(Companion.MAX_BYTES)
|
||||
var bufferSize = 0
|
||||
override fun run() {
|
||||
Log.i("serialReader", "Starting serial loop")
|
||||
while (!isInterrupted) {
|
||||
try {
|
||||
|
||||
/*
|
||||
* check for some bytes, or still bytes still left in
|
||||
* buffer
|
||||
*/
|
||||
if (available() > 0) {
|
||||
val newBytes = read(buffer, bufferSize, Companion.MAX_BYTES - bufferSize)
|
||||
if (newBytes > 0) bufferSize += newBytes
|
||||
Log.d(BMX_BLUETOOTH, "read $newBytes")
|
||||
}
|
||||
if (bufferSize > 0) {
|
||||
val read = messageHandler.read(bufferSize, buffer)
|
||||
|
||||
// shift unread data to start of buffer
|
||||
if (read > 0) {
|
||||
var index = 0
|
||||
for (i in read until bufferSize) {
|
||||
buffer[index++] = buffer[i]
|
||||
}
|
||||
bufferSize = index
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
sleep(10)
|
||||
} catch (ie: InterruptedException) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(BMX_BLUETOOTH, "Error reading serial data", e)
|
||||
}
|
||||
}
|
||||
Log.i(BMX_BLUETOOTH, "Shutting serial loop")
|
||||
}
|
||||
|
||||
// companion object {
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads from the serial buffer, processing any available messages. Must return the number of bytes
|
||||
* consumer from the buffer
|
||||
*
|
||||
* @author jpetrocik
|
||||
*/
|
||||
fun interface MessageHandler {
|
||||
fun read(bufferSize: Int, buffer: ByteArray?): Int
|
||||
}
|
||||
|
||||
fun close() {
|
||||
connected = false
|
||||
if (connectionTask != null) {
|
||||
connectionTask!!.cancel(false)
|
||||
}
|
||||
if (serialReader != null) {
|
||||
serialReader!!.interrupt()
|
||||
try {
|
||||
serialReader!!.join(1000)
|
||||
} catch (ie: InterruptedException) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
serialInputStream!!.close()
|
||||
} catch (e: Exception) {
|
||||
Log.e(BMX_BLUETOOTH, "Failed releasing inputstream connection")
|
||||
}
|
||||
try {
|
||||
serialOutputStream!!.close()
|
||||
} catch (e: Exception) {
|
||||
Log.e(BMX_BLUETOOTH, "Failed releasing outputstream connection")
|
||||
}
|
||||
try {
|
||||
serialSocket!!.close()
|
||||
} catch (e: Exception) {
|
||||
Log.e(BMX_BLUETOOTH, "Failed closing socket")
|
||||
}
|
||||
Log.i(BMX_BLUETOOTH, "Released bluetooth connections")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BMX_BLUETOOTH = "BMXBluetooth"
|
||||
var BLUETOOTH_CONNECTED = "bluetooth-connection-started"
|
||||
var BLUETOOTH_DISCONNECTED = "bluetooth-connection-lost"
|
||||
var BLUETOOTH_FAILED = "bluetooth-connection-failed"
|
||||
private const val MAX_BYTES = 125
|
||||
}
|
||||
}
|
63
app/src/main/java/eu/ztsh/garmin/data/DataCache.kt
Normal file
63
app/src/main/java/eu/ztsh/garmin/data/DataCache.kt
Normal file
|
@ -0,0 +1,63 @@
|
|||
package eu.ztsh.garmin.data
|
||||
|
||||
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
|
||||
import com.mapbox.navigation.core.trip.session.NavigationSessionState
|
||||
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 {
|
||||
|
||||
private var maneuverCache: Maneuver? = null
|
||||
private var locationCache: LocationMatcherResult? = null
|
||||
private var session: NavigationSessionState? = null
|
||||
private var eta: Arrival? = null
|
||||
|
||||
// maneuver
|
||||
fun hasChanged(guidance: Lane?): Boolean {
|
||||
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) {
|
||||
maneuverCache = maneuver
|
||||
}
|
||||
|
||||
// eta
|
||||
fun hasChanged(eta: Arrival): Boolean {
|
||||
return this.eta.let { it == null || it != eta }
|
||||
}
|
||||
|
||||
fun update(eta: Arrival) {
|
||||
this.eta = eta
|
||||
}
|
||||
|
||||
// location
|
||||
fun hasChanged(locationMatcherResult: LocationMatcherResult): Boolean {
|
||||
return locationCache.let { it == null
|
||||
|| it.enhancedLocation.speed != locationMatcherResult.enhancedLocation.speed
|
||||
|| it.speedLimitInfo.speed != locationMatcherResult.speedLimitInfo.speed }
|
||||
}
|
||||
|
||||
fun update(locationMatcherResult: LocationMatcherResult) {
|
||||
locationCache = locationMatcherResult
|
||||
}
|
||||
|
||||
// session
|
||||
fun isActive(): Boolean {
|
||||
return session != null && session is NavigationSessionState.ActiveGuidance
|
||||
}
|
||||
|
||||
fun update(sessionState: NavigationSessionState) {
|
||||
session = sessionState
|
||||
}
|
||||
|
||||
}
|
152
app/src/main/java/eu/ztsh/garmin/data/GarminMapper.kt
Normal file
152
app/src/main/java/eu/ztsh/garmin/data/GarminMapper.kt
Normal file
|
@ -0,0 +1,152 @@
|
|||
package eu.ztsh.garmin.data
|
||||
|
||||
class GarminMapper {
|
||||
|
||||
companion object {
|
||||
|
||||
fun map(lanes: Lanes): IntArray {
|
||||
return intArrayOf(0x02, lanes.lanes.lanes.sumOf { it.value }, lanes.outlines.lanes.sumOf { it.value })
|
||||
}
|
||||
|
||||
fun map(direction: Direction): IntArray {
|
||||
return toDirectionArray(direction.angle, direction.out, direction.roundabout)
|
||||
}
|
||||
|
||||
fun map(distance: Distance): IntArray {
|
||||
return setDistance(distance.distance, distance.unit)
|
||||
}
|
||||
|
||||
fun map(speed: Speed): IntArray {
|
||||
return setSpeed(speed.speed, speed.limit)
|
||||
}
|
||||
|
||||
fun setTime(hours: Int, minutes: Int, traffic: Boolean = false, flag: Boolean = false): IntArray {
|
||||
val trafficChar = asChar(traffic)
|
||||
val flagChar = asChar(flag)
|
||||
// TODO: needed? Displaying ETA, not remaining
|
||||
return if (hours > 99) {
|
||||
intArrayOf(
|
||||
0x05,
|
||||
trafficChar,
|
||||
asDigit(hours / 1000),
|
||||
asDigit(hours / 100),
|
||||
0x00,
|
||||
asDigit(hours / 10),
|
||||
asDigit(hours),
|
||||
0xff,
|
||||
flagChar
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
0x05,
|
||||
trafficChar,
|
||||
asDigit(hours / 10),
|
||||
asDigit(if (hours == 0) 10 else hours),
|
||||
0xff,
|
||||
asDigit(minutes / 10),
|
||||
asDigit(minutes),
|
||||
0x00,
|
||||
flagChar
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun speedControl(state: Boolean): IntArray {
|
||||
return intArrayOf(0x04, if (state) 0x01 else 0x02)
|
||||
}
|
||||
|
||||
fun cleanDistance(): IntArray {
|
||||
return intArrayOf(0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
}
|
||||
|
||||
private fun toDirectionArray(
|
||||
angle: OutAngle,
|
||||
out: OutType = OutType.Lane,
|
||||
roundabout: OutAngle = OutAngle.AsDirection
|
||||
): IntArray {
|
||||
val param1: Int = when (angle) {
|
||||
OutAngle.LeftDown -> 0x10
|
||||
OutAngle.RightDown -> 0x20
|
||||
else -> out.value
|
||||
}
|
||||
val param2: Int = if (out == OutType.RightRoundabout
|
||||
|| out == OutType.LeftRoundabout
|
||||
) {
|
||||
if (roundabout == OutAngle.AsDirection) angle.value else roundabout.value
|
||||
} else {
|
||||
0x00
|
||||
}
|
||||
val param3: Int =
|
||||
if (angle == OutAngle.LeftDown || angle == OutAngle.RightDown) 0x00 else angle.value
|
||||
return intArrayOf(0x01, param1, param2, param3)
|
||||
}
|
||||
|
||||
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(
|
||||
0x03,
|
||||
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(
|
||||
speed: Int,
|
||||
limit: Int = 0,
|
||||
acc: Boolean = false
|
||||
): IntArray {
|
||||
// TODO: car connection
|
||||
val accChar = asChar(acc)
|
||||
return if (limit > 0) {
|
||||
intArrayOf(
|
||||
0x06,
|
||||
asDigit(speed / 100),
|
||||
asDigit(speed / 10),
|
||||
asDigit(if (speed == 0) 10 else speed),
|
||||
0xff,
|
||||
asDigit(limit / 100),
|
||||
asDigit(limit / 10),
|
||||
asDigit(limit),
|
||||
asChar(speed > limit),
|
||||
accChar
|
||||
)
|
||||
} else {
|
||||
intArrayOf(
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
asDigit(speed / 100),
|
||||
asDigit(speed / 10),
|
||||
asDigit(speed),
|
||||
asChar(false),
|
||||
accChar
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun asDigit(n: Double): Int {
|
||||
return asDigit(n.toInt())
|
||||
}
|
||||
|
||||
private fun asDigit(n: Int): Int {
|
||||
if (n == 0) {
|
||||
return 0
|
||||
}
|
||||
val m = n % 10
|
||||
return if (m == 0) 10 else m
|
||||
}
|
||||
|
||||
private fun asChar(boolean: Boolean): Int {
|
||||
return if (boolean) 0xff else 0x00
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
124
app/src/main/java/eu/ztsh/garmin/data/MapboxMapper.kt
Normal file
124
app/src/main/java/eu/ztsh/garmin/data/MapboxMapper.kt
Normal file
|
@ -0,0 +1,124 @@
|
|||
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.tripdata.progress.model.TripProgressUpdateValue
|
||||
|
||||
class MapboxMapper {
|
||||
|
||||
companion object {
|
||||
|
||||
fun asDirection(maneuver: Maneuver): Direction {
|
||||
val direction = Direction()
|
||||
maneuver.primary.apply {
|
||||
when (this.type) {
|
||||
"roundabout" -> {
|
||||
direction.out = OutType.RightRoundabout
|
||||
}
|
||||
"arrive" -> {
|
||||
direction.out = OutType.Flag
|
||||
direction.angle = OutAngle.Right
|
||||
}
|
||||
"turn" -> {
|
||||
when (this.modifier) {
|
||||
"straight" -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
when (this.modifier) {
|
||||
"right" -> {
|
||||
direction.angle = OutAngle.Right
|
||||
when (this.type) {
|
||||
"roundabout" -> {
|
||||
when (this.degrees!!.toInt()) {
|
||||
in 22..66 -> direction.angle = OutAngle.SharpRight
|
||||
in 67..111 -> direction.angle = OutAngle.Right
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"left" -> {
|
||||
direction.angle = OutAngle.Left
|
||||
when (this.type) {
|
||||
"fork", "off ramp" -> {
|
||||
direction.angle = OutAngle.EasyLeft
|
||||
direction.out = OutType.LongerLane
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return direction
|
||||
}
|
||||
|
||||
fun asDistance(maneuver: Maneuver): Distance {
|
||||
return maneuver.stepDistance.let { step ->
|
||||
step.distanceFormatter.formatDistance(step.distanceRemaining!!).split(" ").let {
|
||||
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())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
149
app/src/main/java/eu/ztsh/garmin/data/Model.kt
Normal file
149
app/src/main/java/eu/ztsh/garmin/data/Model.kt
Normal file
|
@ -0,0 +1,149 @@
|
|||
package eu.ztsh.garmin.data
|
||||
|
||||
enum class OutType(val value: Int) {
|
||||
|
||||
Off(0x00),
|
||||
Lane(0x01),
|
||||
LongerLane(0x02),
|
||||
LeftRoundabout(0x04),
|
||||
RightRoundabout(0x08),
|
||||
Flag(0x40),
|
||||
ArrowOnly(0x80);
|
||||
|
||||
}
|
||||
|
||||
enum class OutAngle(val value: Int) {
|
||||
|
||||
Down(0x01),
|
||||
SharpRight(0x02),
|
||||
Right(0x04),
|
||||
EasyRight(0x08),
|
||||
Straight(0x10),
|
||||
EasyLeft(0x20),
|
||||
Left(0x40),
|
||||
SharpLeft(0x80),
|
||||
LeftDown(0x81),
|
||||
RightDown(0x82),
|
||||
AsDirection(0x00)
|
||||
|
||||
}
|
||||
|
||||
enum class Unit(val value: Int) {
|
||||
|
||||
Any(0),
|
||||
Metres(1),
|
||||
Kilometres(3),
|
||||
Miles(5),
|
||||
Foot(8)
|
||||
|
||||
}
|
||||
|
||||
enum class Lane(val value: Int) {
|
||||
|
||||
DotsRight(0x01),
|
||||
OuterRight(0x02),
|
||||
MiddleRight(0x04),
|
||||
InnerRight(0x08),
|
||||
InnerLeft(0x10),
|
||||
MiddleLeft(0x20),
|
||||
OuterLeft(0x40),
|
||||
DotsLeft(0x80);
|
||||
|
||||
companion object {
|
||||
val iterator = {sortedSetOf(OuterRight, MiddleRight, InnerRight, InnerLeft, MiddleLeft, OuterLeft).iterator()}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Arrows(val lanes: Set<Lane>) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Arrows
|
||||
|
||||
return lanes == other.lanes
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return lanes.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
class Lanes(val outlines: Arrows, val lanes: Arrows)
|
||||
|
||||
class Distance(val distance: Double, val unit: Unit) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Distance
|
||||
|
||||
if (distance != other.distance) return false
|
||||
if (unit != other.unit) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = distance.hashCode()
|
||||
result = 31 * result + unit.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Distance($distance$unit)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Speed(val speed: Int, val limit: Int)
|
||||
|
||||
class Arrival(val hours: Int, val minutes: Int) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Arrival
|
||||
|
||||
if (hours != other.hours) return false
|
||||
if (minutes != other.minutes) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return 61 * hours + minutes
|
||||
}
|
||||
}
|
||||
|
||||
class Direction(
|
||||
var angle: OutAngle = OutAngle.AsDirection,
|
||||
var out: OutType = OutType.Lane,
|
||||
var roundabout: OutAngle = OutAngle.AsDirection
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Direction
|
||||
|
||||
if (angle != other.angle) return false
|
||||
if (out != other.out) return false
|
||||
if (roundabout != other.roundabout) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = angle.hashCode()
|
||||
result = 31 * result + out.hashCode()
|
||||
result = 31 * result + roundabout.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
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,19 +1,80 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:text="Enabled"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<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:id="@+id/enabled_switch"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/maneuverView" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<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>
|
|
@ -1,6 +1,6 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Garmin</string>
|
||||
</resources>
|
||||
<string name="mapbox_access_token">pk.eyJ1IjoibWFwcy16dHNoIiwiYSI6ImNsbDl4YXU4cjA3eW8zcXMzbXdjYjNsN3oifQ.kbDCjthamXvXX_pAdsq3hQ</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
|
|
275
app/src/test/java/eu/ztsh/garmin/data/GarminMapperTest.kt
Normal file
275
app/src/test/java/eu/ztsh/garmin/data/GarminMapperTest.kt
Normal file
|
@ -0,0 +1,275 @@
|
|||
package eu.ztsh.garmin.data
|
||||
|
||||
import eu.ztsh.garmin.Garmin
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class GarminMapperTest {
|
||||
|
||||
@Test
|
||||
fun linesTest() {
|
||||
linesTest(
|
||||
setOf(Lane.DotsLeft),
|
||||
setOf(),
|
||||
intArrayOf(2, 128, 0),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 128, 0, 141, 16, 3)
|
||||
)
|
||||
linesTest(
|
||||
Lane.OuterRight,
|
||||
Lane.OuterLeft,
|
||||
intArrayOf(2, 2, 64),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 2, 64, 203, 16, 3)
|
||||
)
|
||||
linesTest(
|
||||
Lane.MiddleRight,
|
||||
Lane.MiddleLeft,
|
||||
intArrayOf(2, 4, 32),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 4, 32, 233, 16, 3)
|
||||
)
|
||||
linesTest(
|
||||
Lane.InnerRight,
|
||||
Lane.InnerLeft,
|
||||
intArrayOf(2, 8, 16),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 8, 16, 16, 245, 16, 3)
|
||||
)
|
||||
linesTest(
|
||||
Lane.InnerLeft,
|
||||
Lane.InnerRight,
|
||||
intArrayOf(2, 16, 8),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 16, 16, 8, 245, 16, 3)
|
||||
)
|
||||
linesTest(
|
||||
Lane.MiddleLeft,
|
||||
Lane.MiddleRight,
|
||||
intArrayOf(2, 32, 4),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 32, 4, 233, 16, 3)
|
||||
)
|
||||
linesTest(
|
||||
Lane.OuterLeft,
|
||||
Lane.OuterRight,
|
||||
intArrayOf(2, 64, 2),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 64, 2, 203, 16, 3)
|
||||
)
|
||||
linesTest(
|
||||
setOf(Lane.DotsRight),
|
||||
setOf(Lane.OuterRight, Lane.MiddleRight, Lane.InnerRight, Lane.InnerLeft, Lane.MiddleLeft, Lane.OuterLeft),
|
||||
intArrayOf(2, 1, 126),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 1, 126, 142, 16, 3)
|
||||
)
|
||||
linesTest(
|
||||
setOf(),
|
||||
setOf(),
|
||||
intArrayOf(2, 0, 0),
|
||||
intArrayOf(16, 123, 9, 3, 0, 0, 0, 85, 21, 2, 0, 0, 13, 16, 3)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun directionTest() {
|
||||
directionTest(
|
||||
OutAngle.RightDown,
|
||||
intArrayOf(1, 32, 0, 0),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 32, 0, 0, 236, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.SharpRight,
|
||||
intArrayOf(1, 1, 0, 2),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 2, 9, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Right,
|
||||
intArrayOf(1, 1, 0, 4),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 4, 7, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.EasyRight,
|
||||
intArrayOf(1, 1, 0, 8),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 8, 3, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Straight,
|
||||
intArrayOf(1, 1, 0, 16),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 16, 16, 251, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.EasyLeft,
|
||||
intArrayOf(1, 1, 0, 32),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 32, 235, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Left,
|
||||
intArrayOf(1, 1, 0, 64),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 64, 203, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.SharpLeft,
|
||||
intArrayOf(1, 1, 0, 128),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 128, 139, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.LeftDown,
|
||||
intArrayOf(1, 16, 0, 0),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 16, 16, 0, 0, 252, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Down,
|
||||
intArrayOf(1, 1, 0, 1),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 1, 0, 1, 10, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.SharpRight, OutType.LongerLane,
|
||||
intArrayOf(1, 2, 0, 2),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 2, 8, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Right, OutType.LongerLane,
|
||||
intArrayOf(1, 2, 0, 4),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 4, 6, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.EasyRight, OutType.LongerLane,
|
||||
intArrayOf(1, 2, 0, 8),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 8, 2, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Straight, OutType.LongerLane,
|
||||
intArrayOf(1, 2, 0, 16),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 16, 16, 250, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.EasyLeft, OutType.LongerLane,
|
||||
intArrayOf(1, 2, 0, 32),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 32, 234, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Left, OutType.LongerLane,
|
||||
intArrayOf(1, 2, 0, 64),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 64, 202, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.SharpLeft, OutType.LongerLane,
|
||||
intArrayOf(1, 2, 0, 128),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 2, 0, 128, 138, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.SharpRight, OutType.RightRoundabout,
|
||||
intArrayOf(1, 8, 2, 2),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 2, 2, 0, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Right, OutType.RightRoundabout,
|
||||
intArrayOf(1, 8, 4, 4),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 4, 4, 252, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.EasyRight, OutType.RightRoundabout,
|
||||
intArrayOf(1, 8, 8, 8),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 8, 8, 244, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Straight, OutType.RightRoundabout,
|
||||
intArrayOf(1, 8, 16, 16),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 16, 16, 16, 16, 228, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.EasyLeft, OutType.RightRoundabout,
|
||||
intArrayOf(1, 8, 32, 32),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 32, 32, 196, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Left, OutType.RightRoundabout,
|
||||
intArrayOf(1, 8, 64, 64),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 64, 64, 132, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.SharpLeft, OutType.RightRoundabout,
|
||||
intArrayOf(1, 8, 128, 128),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 8, 128, 128, 4, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Left, OutType.Flag,
|
||||
intArrayOf(1, 64, 0, 64),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 64, 0, 64, 140, 16, 3)
|
||||
)
|
||||
directionTest(
|
||||
OutAngle.Right, OutType.Flag,
|
||||
intArrayOf(1, 64, 0, 4),
|
||||
intArrayOf(16, 123, 10, 4, 0, 0, 0, 85, 21, 1, 64, 0, 4, 200, 16, 3)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun distanceTest() {
|
||||
// TODO
|
||||
intArrayOf(3, 0, 9, 9, 0, 9, 0)
|
||||
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 0, 233, 16, 3)
|
||||
intArrayOf(3, 0, 9, 9, 0, 9, 3)
|
||||
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 3, 230, 16, 3)
|
||||
intArrayOf(3, 0, 9, 9, 0, 9, 1)
|
||||
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 1, 232, 16, 3)
|
||||
intArrayOf(3, 0, 9, 9, 0, 9, 8)
|
||||
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 8, 225, 16, 3)
|
||||
intArrayOf(3, 0, 9, 9, 0, 9, 5)
|
||||
intArrayOf(16, 123, 13, 7, 0, 0, 0, 85, 21, 3, 0, 9, 9, 0, 9, 5, 228, 16, 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun speedTest() {
|
||||
// TODO
|
||||
intArrayOf(7, 1)
|
||||
intArrayOf(16, 123, 8, 2, 0, 0, 0, 85, 21, 7, 1, 9, 16, 3)
|
||||
intArrayOf(6, 0, 0, 0, 0, 0, 5, 10, 0, 0)
|
||||
intArrayOf(16, 123, 16, 16, 10, 0, 0, 0, 85, 21, 6, 0, 0, 0, 0, 0, 5, 10, 0, 0, 236, 16, 3)
|
||||
intArrayOf(6, 0, 5, 10, 255, 1, 10, 10, 0, 0)
|
||||
intArrayOf(16, 123, 16, 16, 10, 0, 0, 0, 85, 21, 6, 0, 5, 10, 255, 1, 10, 10, 0, 0, 216, 16, 3)
|
||||
intArrayOf(6, 1, 5, 10, 255, 1, 10, 10, 255, 0)
|
||||
intArrayOf(16, 123, 16, 16, 10, 0, 0, 0, 85, 21, 6, 1, 5, 10, 255, 1, 10, 10, 255, 0, 216, 16, 3)
|
||||
intArrayOf(6, 0, 5, 10, 255, 1, 10, 10, 0, 255)
|
||||
intArrayOf(16, 123, 16, 16, 10, 0, 0, 0, 85, 21, 6, 0, 5, 10, 255, 1, 10, 10, 0, 255, 217, 16, 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun timeTest() {
|
||||
// TODO
|
||||
intArrayOf(5, 0, 2, 2, 255, 2, 2, 0, 0)
|
||||
intArrayOf(16, 123, 15, 9, 0, 0, 0, 85, 21, 5, 0, 2, 2, 255, 2, 2, 0, 0, 247, 16, 3)
|
||||
intArrayOf(5, 255, 2, 2, 255, 2, 2, 0, 0)
|
||||
intArrayOf(16, 123, 15, 9, 0, 0, 0, 85, 21, 5, 255, 2, 2, 255, 2, 2, 0, 0, 248, 16, 3)
|
||||
intArrayOf(5, 0, 2, 2, 255, 2, 2, 0, 255)
|
||||
intArrayOf(16, 123, 15, 9, 0, 0, 0, 85, 21, 5, 0, 2, 2, 255, 2, 2, 0, 255, 248, 16, 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun controlTest() {
|
||||
// TODO
|
||||
intArrayOf(4, 1)
|
||||
intArrayOf(16, 123, 8, 2, 0, 0, 0, 85, 21, 4, 1, 12, 16, 3)
|
||||
intArrayOf(4, 0)
|
||||
intArrayOf(16, 123, 8, 2, 0, 0, 0, 85, 21, 4, 0, 13, 16, 3)
|
||||
}
|
||||
|
||||
private fun linesTest(outlines: Lane, arrows: Lane, expectedRaw: IntArray, expectedBoxed: IntArray) {
|
||||
linesTest(setOf(outlines), setOf(arrows), expectedRaw, expectedBoxed)
|
||||
}
|
||||
|
||||
private fun linesTest(outlines: Set<Lane>, arrows: Set<Lane>, expectedRaw: IntArray, expectedBoxed: IntArray) {
|
||||
val lanes = Lanes(Arrows(arrows), Arrows(outlines))
|
||||
makeAssertions(GarminMapper.map(lanes), expectedRaw, expectedBoxed)
|
||||
}
|
||||
|
||||
private fun directionTest(outAngle: OutAngle, expectedRaw: IntArray, expectedBoxed: IntArray) {
|
||||
directionTest(outAngle, OutType.Lane, expectedRaw, expectedBoxed)
|
||||
}
|
||||
|
||||
private fun directionTest(outAngle: OutAngle, outType: OutType, expectedRaw: IntArray, expectedBoxed: IntArray) {
|
||||
val direction = Direction(outAngle, outType)
|
||||
makeAssertions(GarminMapper.map(direction), expectedRaw, expectedBoxed)
|
||||
}
|
||||
|
||||
private fun makeAssertions(resultRaw: IntArray, expectedRaw: IntArray, expectedBoxed: IntArray) {
|
||||
assertThat(resultRaw).containsExactly(expectedRaw.toTypedArray())
|
||||
val resultBoxed = Garmin.prepareData(resultRaw)
|
||||
assertThat(resultBoxed).containsExactly(expectedBoxed.toTypedArray())
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue