Merge branch 'mapbox'
This commit is contained in:
commit
82a29cb00a
30 changed files with 2384 additions and 580 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +1,2 @@
|
||||||
* text eol=lf
|
* text eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,3 +8,4 @@
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
|
__pycache__/
|
||||||
|
|
|
@ -6,16 +6,18 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'eu.ztsh.garmin'
|
namespace 'eu.ztsh.garmin'
|
||||||
compileSdk 31
|
compileSdk 34
|
||||||
|
ndkVersion "23.2.8568313"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.ztsh.garmin"
|
applicationId "eu.ztsh.garmin"
|
||||||
minSdk 29
|
minSdk 29
|
||||||
targetSdk 31
|
targetSdk 34
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
buildConfigField "String", "MAPBOX_DOWNLOADS_TOKEN", "\"$MAPBOX_DOWNLOADS_TOKEN\""
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -33,27 +35,26 @@ android {
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ext {
|
||||||
|
mapboxVersion = '3.2.0'
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation "com.mapbox.navigationcore:navigation:$mapboxVersion"
|
||||||
// Notification parser
|
implementation "com.mapbox.navigationcore:ui-maps:$mapboxVersion"
|
||||||
implementation 'com.github.3v1n0.GMapsParser:navparser:0.2.1'
|
implementation "com.mapbox.navigationcore:voice:$mapboxVersion"
|
||||||
//
|
implementation "com.mapbox.navigationcore:tripdata:$mapboxVersion"
|
||||||
// // Bluetooth serial
|
implementation "com.mapbox.navigationcore:ui-components:$mapboxVersion"
|
||||||
// implementation 'com.github.harry1453:android-bluetooth-serial:v1.1'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
// implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
// implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
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'
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
|
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
testImplementation 'org.assertj:assertj-core:3.24.2'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
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"
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
android:maxSdkVersion="30" />
|
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" />
|
<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-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
|
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
|
||||||
|
|
||||||
<queries>
|
|
||||||
<package android:name="com.google.android.apps.maps" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
</queries>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
@ -32,15 +26,6 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Garmin"
|
android:theme="@style/Theme.Garmin"
|
||||||
tools:targetApi="31">
|
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
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
|
@ -5,12 +5,19 @@ import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
import android.util.Log
|
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.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.SynchronousQueue
|
import java.util.concurrent.SynchronousQueue
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
class Garmin(
|
class Garmin(
|
||||||
val context: MainActivity,
|
val context: MainActivity,
|
||||||
|
@ -18,48 +25,202 @@ class Garmin(
|
||||||
val adapter: BluetoothAdapter
|
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() {
|
fun start() {
|
||||||
thread = ConnectThread()
|
connection = ConnectThread()
|
||||||
thread.start()
|
connection.start()
|
||||||
}
|
|
||||||
|
|
||||||
fun send(data: IntArray) {
|
maneuvers = ManeuverProcessingThread()
|
||||||
thread.enqueue(data)
|
maneuvers.start()
|
||||||
}
|
|
||||||
|
|
||||||
fun send(data: NavigationData) {
|
trips = TripProgressProcessingThread()
|
||||||
Log.d("SEND_DATA", "$data.remainingDistance.distance")
|
trips.start()
|
||||||
|
|
||||||
|
locations = LocationMatcherProcessingThread()
|
||||||
|
locations.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
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 inner class ConnectThread : Thread() {
|
||||||
|
|
||||||
|
private val queue: SynchronousQueue<IntArray> = SynchronousQueue()
|
||||||
|
private var current: IntArray = intArrayOf()
|
||||||
|
|
||||||
private val socket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
|
private val socket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
context.checkBt()
|
context.checkBt()
|
||||||
device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"))
|
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() {
|
override fun run() {
|
||||||
// Cancel discovery because it otherwise slows down the connection.
|
// Cancel discovery because it otherwise slows down the connection.
|
||||||
context.checkBt()
|
context.checkBt()
|
||||||
adapter.cancelDiscovery()
|
adapter.cancelDiscovery()
|
||||||
socket?.connect()
|
try {
|
||||||
sleep(3000)
|
socket?.connect()
|
||||||
readAll()
|
context.setConnectionStatus(true)
|
||||||
while (true) {
|
sleep(3000)
|
||||||
queue.poll()?.let { send(it) }
|
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 buffer = ByteArray(64)
|
||||||
val istr = socket!!.inputStream
|
val istr = socket!!.inputStream
|
||||||
istr!!.let {
|
istr!!.let {
|
||||||
|
@ -82,20 +251,18 @@ class Garmin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun send(data: IntArray) {
|
|
||||||
sendRaw(prepareData(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendRaw(buff: IntArray) {
|
private fun sendRaw(buff: IntArray) {
|
||||||
buff.forEach { socket!!.outputStream.write(it) }
|
buff.forEach { socket!!.outputStream.write(it) }
|
||||||
socket!!.outputStream.flush()
|
socket!!.outputStream.flush()
|
||||||
sleep(2000)
|
|
||||||
readAll()
|
readAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
lateinit var instance: Garmin
|
||||||
|
|
||||||
fun prepareData(input: IntArray): IntArray {
|
fun prepareData(input: IntArray): IntArray {
|
||||||
val n = input.size
|
val n = input.size
|
||||||
var crc = (0xeb + n + n).toUInt()
|
var crc = (0xeb + n + n).toUInt()
|
||||||
|
|
|
@ -2,94 +2,79 @@ package eu.ztsh.garmin
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.bluetooth.BluetoothSocket
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.CompoundButton
|
import android.view.WindowManager
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultCallback
|
import androidx.activity.result.ActivityResultCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import 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 eu.ztsh.garmin.databinding.ActivityMainBinding
|
||||||
import me.trevi.navparser.lib.NavigationData
|
import eu.ztsh.garmin.mapbox.MapControl
|
||||||
import me.trevi.navparser.service.*
|
import eu.ztsh.garmin.util.PermissionsHelper
|
||||||
import java.io.IOException
|
import java.lang.ref.WeakReference
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
var navigating: Boolean = false
|
// lateinit var garmin: Garmin
|
||||||
lateinit var garmin: Garmin
|
|
||||||
private lateinit var binding : ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private val navDataModel: NavigationDataModel by viewModels()
|
private lateinit var mapControl: MapControl
|
||||||
|
|
||||||
|
private val permissionsHelper = PermissionsHelper(WeakReference(this))
|
||||||
|
|
||||||
|
val mapboxNavigation: MapboxNavigation by requireMapboxNavigation(
|
||||||
|
onResumedObserver = object : DefaultLifecycleObserver, MapboxNavigationObserver {
|
||||||
|
override fun onAttached(mapboxNavigation: MapboxNavigation) {
|
||||||
|
mapControl.onAttached(mapboxNavigation)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetached(mapboxNavigation: MapboxNavigation) {
|
||||||
|
mapControl.onDetached(mapboxNavigation)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onInitialize = fun() {
|
||||||
|
mapControl.initNavigation()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
binding.mapView
|
||||||
|
permissionsHelper.checkPermissions {
|
||||||
|
mapControl = MapControl(this, UI(binding), resources)
|
||||||
|
mapControl.init()
|
||||||
|
|
||||||
|
MapboxNavigationApp.setup(
|
||||||
|
NavigationOptions.Builder(applicationContext)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
bluetoothInit()
|
bluetoothInit()
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
binding.enabledSwitch.setOnCheckedChangeListener {
|
|
||||||
_, isChecked ->
|
|
||||||
run {
|
|
||||||
navigating = isChecked
|
|
||||||
|
|
||||||
setNavigationData(NavigationData(true))
|
|
||||||
navDataModel.liveData.observe(this) { setNavigationData(it) }
|
|
||||||
Log.d(TAG, "OK")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
|
|
||||||
startServiceListener()
|
override fun onDestroy() {
|
||||||
checkNotificationsAccess()
|
super.onDestroy()
|
||||||
}
|
mapControl.onDestroy()
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bluetoothInit() {
|
private fun bluetoothInit() {
|
||||||
|
@ -108,10 +93,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
val context = this
|
val context = this
|
||||||
pairedDevices?.firstOrNull { device ->
|
pairedDevices?.firstOrNull { device ->
|
||||||
Log.d(TAG, device.name)
|
Log.d(TAG, device.name)
|
||||||
device.name.equals("GARMIN HUD")
|
device.name == "GARMIN HUD"
|
||||||
}?.apply {
|
}?.apply {
|
||||||
garmin = Garmin(context, this, bluetoothAdapter)
|
Garmin.instance = Garmin(context, this, bluetoothAdapter)
|
||||||
garmin.start()
|
Garmin.instance.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -138,33 +123,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startServiceListener() {
|
fun setConnectionStatus(success: Boolean) {
|
||||||
Log.d(TAG, "Start Service Listener")
|
this.runOnUiThread {
|
||||||
setServiceListenerIntent(createPendingResult(100, Intent(), 0))
|
if (success) {
|
||||||
}
|
Toast.makeText(this, "Garmin connected", Toast.LENGTH_LONG).show()
|
||||||
|
} else {
|
||||||
fun stopServiceListener() {
|
// TODO: Make snackbar with reconnect option
|
||||||
Log.d(TAG, "Stopping Service Listener")
|
Toast.makeText(this, "Garmin not connected", Toast.LENGTH_LONG).show()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "bt"
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
android:layout_width="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
android:layout_height="match_parent">
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
<com.mapbox.maps.MapView
|
||||||
tools:context=".MainActivity">
|
android:id="@+id/mapView"
|
||||||
<androidx.appcompat.widget.SwitchCompat
|
android:layout_width="0dp"
|
||||||
android:text="Enabled"
|
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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:id="@+id/enabled_switch"
|
android:layout_marginTop="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:layout_marginEnd="16dp"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
android:visibility="invisible"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
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">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
<item name="colorPrimary">@color/purple_200</item>
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Garmin</string>
|
<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">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Garmin" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/purple_500</item>
|
<item name="colorPrimary">@color/purple_500</item>
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '7.4.1' apply false
|
id 'com.android.application' version '8.2.0' apply false
|
||||||
id 'com.android.library' version '7.4.1' apply false
|
id 'com.android.library' version '8.2.0' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.8.21' apply false
|
id 'org.jetbrains.kotlin.android' version '1.9.24' apply false
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
#Thu Jun 22 21:51:56 CEST 2023
|
#Thu Jun 22 21:51:56 CEST 2023
|
||||||
android.enableJetifier=true
|
android.enableJetifier=false
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
#Thu Jun 22 21:00:17 CEST 2023
|
#Thu Jun 22 21:00:17 CEST 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
178
gradlew.bat
vendored
178
gradlew.bat
vendored
|
@ -1,89 +1,89 @@
|
||||||
@rem
|
@rem
|
||||||
@rem Copyright 2015 the original author or authors.
|
@rem Copyright 2015 the original author or authors.
|
||||||
@rem
|
@rem
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@rem you may not use this file except in compliance with the License.
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables with windows NT shell
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
echo.
|
echo.
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
echo location of your Java installation.
|
echo location of your Java installation.
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
:omega
|
:omega
|
||||||
|
|
200
python/main.py
Normal file
200
python/main.py
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
import time
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from numpy import uint8
|
||||||
|
|
||||||
|
gps = False
|
||||||
|
|
||||||
|
|
||||||
|
class OutType(Enum):
|
||||||
|
Off = 0x00
|
||||||
|
Lane = 0x01
|
||||||
|
LongerLane = 0x02
|
||||||
|
LeftRoundabout = 0x04
|
||||||
|
RightRoundabout = 0x08
|
||||||
|
Flag = 0x40
|
||||||
|
ArrowOnly = 0x80
|
||||||
|
|
||||||
|
|
||||||
|
class OutAngle(Enum):
|
||||||
|
Down = 0x01
|
||||||
|
SharpRight = 0x02
|
||||||
|
Right = 0x04
|
||||||
|
EasyRight = 0x08
|
||||||
|
Straight = 0x10
|
||||||
|
EasyLeft = 0x20
|
||||||
|
Left = 0x40
|
||||||
|
SharpLeft = 0x80
|
||||||
|
LeftDown = 0x81
|
||||||
|
RightDown = 0x82
|
||||||
|
AsDirection = 0x00
|
||||||
|
|
||||||
|
|
||||||
|
class Unit(Enum):
|
||||||
|
Any = 0
|
||||||
|
Metres = 1
|
||||||
|
Kilometres = 3
|
||||||
|
Miles = 5
|
||||||
|
Foot = 8
|
||||||
|
|
||||||
|
|
||||||
|
class Lane(Enum):
|
||||||
|
DotsRight = 0x01
|
||||||
|
OuterRight = 0x02
|
||||||
|
MiddleRight = 0x04
|
||||||
|
InnerRight = 0x08
|
||||||
|
InnerLeft = 0x10
|
||||||
|
MiddleLeft = 0x20
|
||||||
|
OuterLeft = 0x40
|
||||||
|
DotsLeft = 0x80
|
||||||
|
|
||||||
|
|
||||||
|
class Controller:
|
||||||
|
|
||||||
|
def __init__(self, device: str = None):
|
||||||
|
self.gps = False
|
||||||
|
self.debug = device is None
|
||||||
|
if not self.debug:
|
||||||
|
self.ser = serial.Serial(device, 9600)
|
||||||
|
time.sleep(2)
|
||||||
|
print(self.ser.read_all())
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.send_hud([0x03, 0, 0, 0, 0x00, 0, 0])
|
||||||
|
|
||||||
|
def _as_dgt(self, number: float) -> int:
|
||||||
|
n = int(number)
|
||||||
|
if n == 0:
|
||||||
|
return 0
|
||||||
|
n %= 10
|
||||||
|
return 10 if n == 0 else n
|
||||||
|
|
||||||
|
def set_lines(self, outlines: list, arrows: list):
|
||||||
|
self.send_hud([0x02, sum([lane.value for lane in outlines]), sum([lane.value for lane in arrows])])
|
||||||
|
|
||||||
|
def set_direction(self, angle: OutAngle, out: OutType = OutType.Lane, roundabout: OutAngle = OutAngle.AsDirection):
|
||||||
|
if angle == OutAngle.LeftDown:
|
||||||
|
param_1 = 0x10
|
||||||
|
elif angle == OutAngle.RightDown:
|
||||||
|
param_1 = 0x20
|
||||||
|
else:
|
||||||
|
param_1 = out.value
|
||||||
|
|
||||||
|
if out == OutType.RightRoundabout or out == OutType.LeftRoundabout:
|
||||||
|
param_2 = angle.value if roundabout == OutAngle.AsDirection else roundabout.value
|
||||||
|
else:
|
||||||
|
param_2 = 0x00
|
||||||
|
|
||||||
|
param_3 = 0x00 if angle == OutAngle.LeftDown or angle == OutAngle.RightDown else angle.value
|
||||||
|
arr = [0x01, param_1, param_2, param_3]
|
||||||
|
self.send_hud(arr)
|
||||||
|
|
||||||
|
def set_distance(self, distance: float, unit: Unit = Unit.Any):
|
||||||
|
is_float = int(distance * 10) == int(distance) * 10
|
||||||
|
if not is_float:
|
||||||
|
distance = distance * 10
|
||||||
|
arr = [0x03,
|
||||||
|
self._as_dgt(distance // 1000),
|
||||||
|
self._as_dgt(distance // 100),
|
||||||
|
self._as_dgt(distance // 10),
|
||||||
|
0x00 if is_float else 0xff,
|
||||||
|
self._as_dgt(distance),
|
||||||
|
unit.value]
|
||||||
|
self.send_hud(arr)
|
||||||
|
|
||||||
|
def set_speed(self, speed: int, limit: int = 0, acc: bool = False):
|
||||||
|
acc_char = 0xff if acc else 0x00
|
||||||
|
if not self.gps:
|
||||||
|
self.set_distance(speed)
|
||||||
|
self.send_hud([0x06, 0, 0, 0, 0, 0, 0, 0, 0, acc_char])
|
||||||
|
return
|
||||||
|
if limit > 0:
|
||||||
|
arr = [0x06, self._as_dgt(speed // 100), self._as_dgt(speed // 10), self._as_dgt(speed), 0xff,
|
||||||
|
self._as_dgt(limit // 100), self._as_dgt(limit // 10), self._as_dgt(limit),
|
||||||
|
0xff if speed > limit else 0x00, acc_char]
|
||||||
|
else:
|
||||||
|
arr = [0x06, 0x00, 0x00, 0x00, 0x00, self._as_dgt(speed // 100), self._as_dgt(speed // 10),
|
||||||
|
self._as_dgt(speed), 0x00, acc_char]
|
||||||
|
self.send_hud(arr)
|
||||||
|
|
||||||
|
def set_time(self, hours: int, minutes: int, traffic: bool = False, flag: bool = False):
|
||||||
|
traffic_char = 0xff if traffic else 0x00
|
||||||
|
flag_char = 0xff if flag else 0x00
|
||||||
|
if hours > 99:
|
||||||
|
arr = [0x05, traffic_char, self._as_dgt(hours // 1000), self._as_dgt(hours // 100), 0x00,
|
||||||
|
self._as_dgt(hours // 10), self._as_dgt(hours), 0xff, flag_char]
|
||||||
|
else:
|
||||||
|
arr = [0x05, traffic_char, self._as_dgt(hours // 10), self._as_dgt(hours), 0xff,
|
||||||
|
self._as_dgt(minutes // 10), self._as_dgt(minutes), 0x00, flag_char]
|
||||||
|
self.send_hud(arr)
|
||||||
|
|
||||||
|
def set_gps(self, state: bool):
|
||||||
|
if state:
|
||||||
|
self.send_hud([0x07, 0x01])
|
||||||
|
self.gps = state
|
||||||
|
|
||||||
|
def set_speed_control(self, on: bool = True):
|
||||||
|
self.send_hud([0x04, 0x01 if on else 0x00])
|
||||||
|
|
||||||
|
def set_compass(self, direction: float):
|
||||||
|
if direction > 337.5 or direction <= 22.5:
|
||||||
|
self.set_direction(OutAngle.Straight, OutType.ArrowOnly)
|
||||||
|
elif 22.5 < direction <= 67.5:
|
||||||
|
self.set_direction(OutAngle.EasyRight, OutType.ArrowOnly)
|
||||||
|
elif 67.5 < direction <= 112.5:
|
||||||
|
self.set_direction(OutAngle.Right, OutType.ArrowOnly)
|
||||||
|
elif 112.5 < direction <= 157.5:
|
||||||
|
self.set_direction(OutAngle.SharpRight, OutType.ArrowOnly)
|
||||||
|
elif 157.5 < direction <= 202.5:
|
||||||
|
self.set_direction(OutAngle.Down, OutType.ArrowOnly)
|
||||||
|
elif 202.5 < direction <= 247.5:
|
||||||
|
self.set_direction(OutAngle.SharpLeft, OutType.ArrowOnly)
|
||||||
|
elif 247.5 < direction <= 292.5:
|
||||||
|
self.set_direction(OutAngle.Left, OutType.ArrowOnly)
|
||||||
|
elif 292.5 < direction <= 337.5:
|
||||||
|
self.set_direction(OutAngle.EasyLeft, OutType.ArrowOnly)
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def send_hud(self, buf: list):
|
||||||
|
print("buf", buf)
|
||||||
|
n = len(buf)
|
||||||
|
chars = []
|
||||||
|
crc = uint8(0xeb + n + n)
|
||||||
|
chars.append(0x10)
|
||||||
|
chars.append(0x7b)
|
||||||
|
chars.append(n + 6)
|
||||||
|
if n == 0xa:
|
||||||
|
chars.append(0x10)
|
||||||
|
chars.append(n)
|
||||||
|
chars.append(0x00)
|
||||||
|
chars.append(0x00)
|
||||||
|
chars.append(0x00)
|
||||||
|
chars.append(0x55)
|
||||||
|
chars.append(0x15)
|
||||||
|
for char in buf:
|
||||||
|
crc = uint8(crc + char)
|
||||||
|
chars.append(char)
|
||||||
|
if char == 0x10:
|
||||||
|
chars.append(0x10)
|
||||||
|
chars.append((-crc) & 0xff)
|
||||||
|
chars.append(0x10)
|
||||||
|
chars.append(0x03)
|
||||||
|
self.send_packet(chars)
|
||||||
|
|
||||||
|
def send_packet(self, buff):
|
||||||
|
encoded = [bytes(chr(char), 'raw_unicode_escape') for char in buff]
|
||||||
|
if self.debug:
|
||||||
|
print("raw", buff)
|
||||||
|
print("enc", encoded)
|
||||||
|
else:
|
||||||
|
for char in buff:
|
||||||
|
self.ser.write(bytes(chr(char), 'raw_unicode_escape'))
|
||||||
|
time.sleep(0.2)
|
||||||
|
print(self.ser.read_all())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
device = sys.argv[1] if len(sys.argv) > 1 else None
|
||||||
|
c = Controller(device)
|
141
python/test.py
Normal file
141
python/test.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
from main import Controller, Lane, OutType, OutAngle, Unit
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
interval = 0.2
|
||||||
|
|
||||||
|
|
||||||
|
def suite(controller: Controller):
|
||||||
|
lines(controller)
|
||||||
|
direction(controller)
|
||||||
|
distance(controller)
|
||||||
|
speed(controller)
|
||||||
|
time(controller)
|
||||||
|
control(controller)
|
||||||
|
compass(controller)
|
||||||
|
|
||||||
|
|
||||||
|
def lines(controller: Controller):
|
||||||
|
print("Lines")
|
||||||
|
controller.set_lines([Lane.DotsLeft], [])
|
||||||
|
controller.set_lines([Lane.OuterRight], [Lane.OuterLeft])
|
||||||
|
controller.set_lines([Lane.MiddleRight], [Lane.MiddleLeft])
|
||||||
|
controller.set_lines([Lane.InnerRight], [Lane.InnerLeft])
|
||||||
|
controller.set_lines([Lane.InnerLeft], [Lane.InnerRight])
|
||||||
|
controller.set_lines([Lane.MiddleLeft], [Lane.MiddleRight])
|
||||||
|
controller.set_lines([Lane.OuterLeft], [Lane.OuterRight])
|
||||||
|
controller.set_lines(
|
||||||
|
[Lane.DotsRight],
|
||||||
|
[Lane.OuterRight, Lane.MiddleRight, Lane.InnerRight, Lane.InnerLeft, Lane.MiddleLeft, Lane.OuterLeft]
|
||||||
|
)
|
||||||
|
controller.set_lines([], [])
|
||||||
|
|
||||||
|
|
||||||
|
def direction(controller: Controller):
|
||||||
|
print("Direction")
|
||||||
|
controller.set_direction(OutAngle.SharpRight)
|
||||||
|
controller.set_direction(OutAngle.Right)
|
||||||
|
controller.set_direction(OutAngle.EasyRight)
|
||||||
|
controller.set_direction(OutAngle.Straight)
|
||||||
|
controller.set_direction(OutAngle.EasyLeft)
|
||||||
|
controller.set_direction(OutAngle.Left)
|
||||||
|
controller.set_direction(OutAngle.SharpLeft)
|
||||||
|
controller.set_direction(OutAngle.LeftDown)
|
||||||
|
controller.set_direction(OutAngle.RightDown)
|
||||||
|
controller.set_direction(OutAngle.Down)
|
||||||
|
controller.set_direction(OutAngle.SharpRight, OutType.LongerLane)
|
||||||
|
controller.set_direction(OutAngle.Right, OutType.LongerLane)
|
||||||
|
controller.set_direction(OutAngle.EasyRight, OutType.LongerLane)
|
||||||
|
controller.set_direction(OutAngle.Straight, OutType.LongerLane)
|
||||||
|
controller.set_direction(OutAngle.EasyLeft, OutType.LongerLane)
|
||||||
|
controller.set_direction(OutAngle.Left, OutType.LongerLane)
|
||||||
|
controller.set_direction(OutAngle.SharpLeft, OutType.LongerLane)
|
||||||
|
roundabout(controller)
|
||||||
|
controller.set_direction(OutAngle.Left, OutType.Flag)
|
||||||
|
controller.set_direction(OutAngle.Right, OutType.Flag)
|
||||||
|
|
||||||
|
|
||||||
|
def roundabout(controller: Controller):
|
||||||
|
controller.set_direction(OutAngle.SharpRight, OutType.RightRoundabout)
|
||||||
|
controller.set_direction(OutAngle.Right, OutType.RightRoundabout)
|
||||||
|
controller.set_direction(OutAngle.EasyRight, OutType.RightRoundabout)
|
||||||
|
controller.set_direction(OutAngle.Straight, OutType.RightRoundabout)
|
||||||
|
controller.set_direction(OutAngle.EasyLeft, OutType.RightRoundabout)
|
||||||
|
controller.set_direction(OutAngle.Left, OutType.RightRoundabout)
|
||||||
|
controller.set_direction(OutAngle.SharpLeft, OutType.RightRoundabout)
|
||||||
|
controller.set_direction(OutAngle.Down, OutType.RightRoundabout)
|
||||||
|
|
||||||
|
|
||||||
|
def distance(controller: Controller):
|
||||||
|
print("Distance")
|
||||||
|
controller.set_distance(555.5)
|
||||||
|
controller.set_distance(6666)
|
||||||
|
controller.set_distance(777.7)
|
||||||
|
controller.set_distance(888, Unit.Kilometres)
|
||||||
|
controller.set_distance(999, Unit.Metres)
|
||||||
|
controller.set_distance(999, Unit.Foot)
|
||||||
|
controller.set_distance(999, Unit.Miles)
|
||||||
|
|
||||||
|
|
||||||
|
def speed(controller: Controller):
|
||||||
|
print("Speed")
|
||||||
|
controller.set_gps(True)
|
||||||
|
controller.set_speed(50)
|
||||||
|
controller.set_speed(50, 100)
|
||||||
|
controller.set_speed(150, 100)
|
||||||
|
controller.set_speed(50, 100, True)
|
||||||
|
|
||||||
|
|
||||||
|
def time(controller: Controller):
|
||||||
|
print("Time")
|
||||||
|
controller.set_time(22, 22)
|
||||||
|
controller.set_time(22, 22, traffic=True)
|
||||||
|
controller.set_time(22, 22, flag=True)
|
||||||
|
|
||||||
|
|
||||||
|
def control(controller: Controller):
|
||||||
|
print("Speed Control")
|
||||||
|
controller.set_speed_control()
|
||||||
|
controller.set_speed_control(False)
|
||||||
|
|
||||||
|
|
||||||
|
def compass(controller: Controller):
|
||||||
|
print("Compass")
|
||||||
|
controller.set_compass(22.5)
|
||||||
|
controller.set_compass(67.5)
|
||||||
|
controller.set_compass(112.5)
|
||||||
|
controller.set_compass(157.5)
|
||||||
|
controller.set_compass(202.5)
|
||||||
|
controller.set_compass(247.5)
|
||||||
|
controller.set_compass(292.5)
|
||||||
|
controller.set_compass(337.5)
|
||||||
|
|
||||||
|
|
||||||
|
def route(controller: Controller):
|
||||||
|
print("Route")
|
||||||
|
controller.set_direction(OutAngle.Left)
|
||||||
|
controller.set_gps(True)
|
||||||
|
controller.set_speed(50, 50)
|
||||||
|
controller.set_distance(1.2, Unit.Kilometres)
|
||||||
|
sleep(1)
|
||||||
|
controller.set_distance(1.1, Unit.Kilometres)
|
||||||
|
sleep(1)
|
||||||
|
controller.set_distance(1, Unit.Kilometres)
|
||||||
|
sleep(1)
|
||||||
|
remaining = 900
|
||||||
|
while remaining > 0:
|
||||||
|
controller.set_distance(remaining, Unit.Metres)
|
||||||
|
sleep(1)
|
||||||
|
remaining -= 100
|
||||||
|
controller.set_direction(OutAngle.Right, OutType.Flag)
|
||||||
|
remaining = 900
|
||||||
|
while remaining > 0:
|
||||||
|
controller.set_distance(remaining, Unit.Metres)
|
||||||
|
sleep(1)
|
||||||
|
remaining -= 100
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import os
|
||||||
|
name = '/dev/rfcomm0' if os.name != 'nt' else 'COM8'
|
||||||
|
instance = Controller(name)
|
||||||
|
suite(instance)
|
|
@ -13,6 +13,19 @@ dependencyResolutionManagement {
|
||||||
maven {
|
maven {
|
||||||
url 'https://jitpack.io'
|
url 'https://jitpack.io'
|
||||||
}
|
}
|
||||||
|
maven {
|
||||||
|
url 'https://api.mapbox.com/downloads/v2/releases/maven'
|
||||||
|
authentication {
|
||||||
|
basic(BasicAuthentication)
|
||||||
|
}
|
||||||
|
credentials {
|
||||||
|
// Do not change the username below.
|
||||||
|
// This should always be `mapbox` (not your username).
|
||||||
|
username = "mapbox"
|
||||||
|
// Use the secret token you stored in gradle.properties as the password
|
||||||
|
password = MAPBOX_DOWNLOADS_TOKEN
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootProject.name = "Garmin"
|
rootProject.name = "Garmin"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue