Merge pull request 'feat: search engine' (#7) from feat/issue.3.search into master
Reviewed-on: https://hattori.ztsh.eu/stawros/Garmin/pulls/7
This commit is contained in:
commit
fb62c8dd77
11 changed files with 445 additions and 7 deletions
|
@ -40,14 +40,27 @@ android {
|
||||||
}
|
}
|
||||||
ext {
|
ext {
|
||||||
mapboxVersion = '3.2.0'
|
mapboxVersion = '3.2.0'
|
||||||
|
searchApiVersion = '2.3.1'
|
||||||
|
searchNativeVersion = '2.2.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation 'com.mapbox.maps:android:11.5.1'
|
||||||
|
|
||||||
implementation "com.mapbox.navigationcore:navigation:$mapboxVersion"
|
implementation "com.mapbox.navigationcore:navigation:$mapboxVersion"
|
||||||
implementation "com.mapbox.navigationcore:ui-maps:$mapboxVersion"
|
implementation "com.mapbox.navigationcore:ui-maps:$mapboxVersion"
|
||||||
implementation "com.mapbox.navigationcore:voice:$mapboxVersion"
|
implementation "com.mapbox.navigationcore:voice:$mapboxVersion"
|
||||||
implementation "com.mapbox.navigationcore:tripdata:$mapboxVersion"
|
implementation "com.mapbox.navigationcore:tripdata:$mapboxVersion"
|
||||||
implementation "com.mapbox.navigationcore:ui-components:$mapboxVersion"
|
implementation "com.mapbox.navigationcore:ui-components:$mapboxVersion"
|
||||||
|
|
||||||
|
implementation "com.mapbox.search:base:$searchApiVersion"
|
||||||
|
implementation "com.mapbox.search:autofill:$searchApiVersion"
|
||||||
|
implementation "com.mapbox.search:place-autocomplete:$searchApiVersion"
|
||||||
|
implementation "com.mapbox.search:mapbox-search-android:$searchApiVersion"
|
||||||
|
implementation "com.mapbox.search:mapbox-search-android-ui:$searchApiVersion"
|
||||||
|
implementation "com.mapbox.search:mapbox-search-android-native:$searchNativeVersion"
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
|
|
|
@ -27,6 +27,7 @@ import eu.ztsh.garmin.mapbox.MapControl
|
||||||
import eu.ztsh.garmin.util.PermissionsHelper
|
import eu.ztsh.garmin.util.PermissionsHelper
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
|
|
@ -56,4 +56,7 @@ class UI(b: ActivityMainBinding) {
|
||||||
val routeOverview = b.routeOverview
|
val routeOverview = b.routeOverview
|
||||||
val stop = b.stop
|
val stop = b.stop
|
||||||
|
|
||||||
|
val searchResultsView = b.searchResults
|
||||||
|
val searchPlaceView = b.searchPlaces
|
||||||
|
val queryEditText = b.query
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
import com.mapbox.maps.ImageHolder
|
import com.mapbox.maps.ImageHolder
|
||||||
import com.mapbox.maps.plugin.LocationPuck2D
|
import com.mapbox.maps.plugin.LocationPuck2D
|
||||||
import com.mapbox.maps.plugin.animation.camera
|
import com.mapbox.maps.plugin.animation.camera
|
||||||
|
@ -48,6 +49,8 @@ class MapControl(
|
||||||
*/
|
*/
|
||||||
val navigationLocationProvider = NavigationLocationProvider()
|
val navigationLocationProvider = NavigationLocationProvider()
|
||||||
|
|
||||||
|
val navigationStatusControl = NavigationStatusControl()
|
||||||
|
|
||||||
val replay = ReplayResources(this)
|
val replay = ReplayResources(this)
|
||||||
|
|
||||||
// Observers
|
// Observers
|
||||||
|
@ -58,6 +61,7 @@ class MapControl(
|
||||||
private lateinit var locationObserver: LocationObserver
|
private lateinit var locationObserver: LocationObserver
|
||||||
private lateinit var routeProgressObserver: RouteProgressObserver
|
private lateinit var routeProgressObserver: RouteProgressObserver
|
||||||
private lateinit var voiceInstructionsObserver: VoiceInstructionsObserver
|
private lateinit var voiceInstructionsObserver: VoiceInstructionsObserver
|
||||||
|
private val searchControl = SearchControl(this, ui)
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
viewportDataSource = MapboxNavigationViewportDataSource(ui.mapView.mapboxMap)
|
viewportDataSource = MapboxNavigationViewportDataSource(ui.mapView.mapboxMap)
|
||||||
|
@ -103,6 +107,10 @@ class MapControl(
|
||||||
voiceInstructionsObserver = voiceControl.voiceInstructionsObserver
|
voiceInstructionsObserver = voiceControl.voiceInstructionsObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun routeToPoint(point: Point) {
|
||||||
|
routeControl.findRoute(point)
|
||||||
|
}
|
||||||
|
|
||||||
fun initNavigation() {
|
fun initNavigation() {
|
||||||
MapboxNavigationApp.setup(
|
MapboxNavigationApp.setup(
|
||||||
NavigationOptions.Builder(context)
|
NavigationOptions.Builder(context)
|
||||||
|
@ -134,6 +142,8 @@ class MapControl(
|
||||||
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
|
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
|
||||||
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
|
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
|
||||||
|
|
||||||
|
navigationStatusControl.registerObserver(searchControl)
|
||||||
|
|
||||||
replay.onAttached(mapboxNavigation)
|
replay.onAttached(mapboxNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +152,9 @@ class MapControl(
|
||||||
mapboxNavigation.unregisterLocationObserver(locationObserver)
|
mapboxNavigation.unregisterLocationObserver(locationObserver)
|
||||||
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
|
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
|
||||||
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
|
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
|
||||||
|
|
||||||
|
navigationStatusControl.unregisterObserver(searchControl)
|
||||||
|
|
||||||
replay.onDetached(mapboxNavigation)
|
replay.onDetached(mapboxNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
49
app/src/main/java/eu/ztsh/garmin/mapbox/NavigationStatus.kt
Normal file
49
app/src/main/java/eu/ztsh/garmin/mapbox/NavigationStatus.kt
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package eu.ztsh.garmin.mapbox
|
||||||
|
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
class NavigationStatusControl {
|
||||||
|
|
||||||
|
private val stateObservers = CopyOnWriteArraySet<NavigationStatusObserver>()
|
||||||
|
private val current = AtomicReference(NavigationStatus.IDLE)
|
||||||
|
|
||||||
|
fun registerObserver(observer: NavigationStatusObserver) {
|
||||||
|
stateObservers.add(observer)
|
||||||
|
observer.onNavigationStatusChanged(current.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterObserver(observer: NavigationStatusObserver) {
|
||||||
|
stateObservers.remove(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendEvent(status: NavigationStatus) {
|
||||||
|
current.set(status)
|
||||||
|
stateObservers.forEach {
|
||||||
|
it.onNavigationStatusChanged(status)
|
||||||
|
}
|
||||||
|
if (status == NavigationStatus.FINISHED || status == NavigationStatus.CANCELED) {
|
||||||
|
// TODO: lifecyclescope?
|
||||||
|
runBlocking {
|
||||||
|
delay(1000)
|
||||||
|
sendEvent(NavigationStatus.IDLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class NavigationStatus {
|
||||||
|
IDLE,
|
||||||
|
STARTED,
|
||||||
|
FINISHED,
|
||||||
|
CANCELED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface NavigationStatusObserver {
|
||||||
|
|
||||||
|
fun onNavigationStatusChanged(navigationStatus: NavigationStatus)
|
||||||
|
|
||||||
|
}
|
|
@ -3,10 +3,10 @@ package eu.ztsh.garmin.mapbox
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.mapbox.api.directions.v5.models.Bearing
|
import com.mapbox.api.directions.v5.models.Bearing
|
||||||
import com.mapbox.api.directions.v5.models.RouteOptions
|
import com.mapbox.api.directions.v5.models.RouteOptions
|
||||||
import com.mapbox.geojson.Point
|
import com.mapbox.geojson.Point
|
||||||
import com.mapbox.maps.plugin.gestures.gestures
|
|
||||||
import com.mapbox.navigation.base.TimeFormat
|
import com.mapbox.navigation.base.TimeFormat
|
||||||
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
||||||
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
|
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
|
||||||
|
@ -37,6 +37,9 @@ import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
|
||||||
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
|
||||||
import eu.ztsh.garmin.Garmin
|
import eu.ztsh.garmin.Garmin
|
||||||
import eu.ztsh.garmin.UI
|
import eu.ztsh.garmin.UI
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class RouteControl(private val mapControl: MapControl, ui: UI, private val context: Context) {
|
class RouteControl(private val mapControl: MapControl, ui: UI, private val context: Context) {
|
||||||
|
|
||||||
|
@ -115,11 +118,6 @@ class RouteControl(private val mapControl: MapControl, ui: UI, private val conte
|
||||||
// Ensure that the route line related layers are present before the route arrow
|
// Ensure that the route line related layers are present before the route arrow
|
||||||
routeLineView.initializeLayers(it)
|
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
|
// initialize view interactions
|
||||||
|
@ -219,7 +217,7 @@ class RouteControl(private val mapControl: MapControl, ui: UI, private val conte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findRoute(destination: Point) {
|
fun findRoute(destination: Point) {
|
||||||
val originLocation = mapControl.navigationLocationProvider.lastLocation ?: return
|
val originLocation = mapControl.navigationLocationProvider.lastLocation ?: return
|
||||||
val originPoint = Point.fromLngLat(originLocation.longitude, originLocation.latitude)
|
val originPoint = Point.fromLngLat(originLocation.longitude, originLocation.latitude)
|
||||||
|
|
||||||
|
@ -284,6 +282,15 @@ class RouteControl(private val mapControl: MapControl, ui: UI, private val conte
|
||||||
|
|
||||||
// start simulation
|
// start simulation
|
||||||
mapControl.replay.startSimulation(routes.first().directionsRoute)
|
mapControl.replay.startSimulation(routes.first().directionsRoute)
|
||||||
|
|
||||||
|
mapControl.context.apply {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
async {
|
||||||
|
delay(5000)
|
||||||
|
mapControl.navigationCamera.requestNavigationCameraToFollowing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearRouteAndStopNavigation() {
|
private fun clearRouteAndStopNavigation() {
|
||||||
|
@ -298,6 +305,9 @@ class RouteControl(private val mapControl: MapControl, ui: UI, private val conte
|
||||||
mapControl.ui.maneuverView.visibility = View.INVISIBLE
|
mapControl.ui.maneuverView.visibility = View.INVISIBLE
|
||||||
mapControl.ui.routeOverview.visibility = View.INVISIBLE
|
mapControl.ui.routeOverview.visibility = View.INVISIBLE
|
||||||
mapControl.ui.tripProgressCard.visibility = View.INVISIBLE
|
mapControl.ui.tripProgressCard.visibility = View.INVISIBLE
|
||||||
|
|
||||||
|
// post custom event
|
||||||
|
mapControl.navigationStatusControl.sendEvent(NavigationStatus.CANCELED)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
|
|
281
app/src/main/java/eu/ztsh/garmin/mapbox/SearchControl.kt
Normal file
281
app/src/main/java/eu/ztsh/garmin/mapbox/SearchControl.kt
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
package eu.ztsh.garmin.mapbox
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
|
import com.mapbox.maps.CameraOptions
|
||||||
|
import com.mapbox.maps.EdgeInsets
|
||||||
|
import com.mapbox.maps.MapView
|
||||||
|
import com.mapbox.maps.plugin.annotation.annotations
|
||||||
|
import com.mapbox.maps.plugin.annotation.generated.CircleAnnotationOptions
|
||||||
|
import com.mapbox.maps.plugin.annotation.generated.createCircleAnnotationManager
|
||||||
|
import com.mapbox.maps.plugin.gestures.gestures
|
||||||
|
import com.mapbox.search.autocomplete.PlaceAutocomplete
|
||||||
|
import com.mapbox.search.autocomplete.PlaceAutocompleteOptions
|
||||||
|
import com.mapbox.search.autocomplete.PlaceAutocompleteSuggestion
|
||||||
|
import com.mapbox.search.autocomplete.PlaceAutocompleteType
|
||||||
|
import com.mapbox.search.ui.adapter.autocomplete.PlaceAutocompleteUiAdapter
|
||||||
|
import com.mapbox.search.ui.view.CommonSearchViewConfiguration
|
||||||
|
import com.mapbox.search.ui.view.SearchResultsView
|
||||||
|
import com.mapbox.search.ui.view.place.SearchPlace
|
||||||
|
import eu.ztsh.garmin.R
|
||||||
|
import eu.ztsh.garmin.UI
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SearchControl(val mapControl: MapControl, val ui: UI): NavigationStatusObserver {
|
||||||
|
|
||||||
|
// Set your Access Token here if it's not already set in some other way
|
||||||
|
// MapboxOptions.accessToken = "<my-access-token>"
|
||||||
|
private var placeAutocomplete: PlaceAutocomplete = PlaceAutocomplete.create()
|
||||||
|
|
||||||
|
private var placeAutocompleteUiAdapter: PlaceAutocompleteUiAdapter
|
||||||
|
|
||||||
|
private val mapMarkersManager: MapMarkersManager = MapMarkersManager(ui.mapView)
|
||||||
|
|
||||||
|
private var ignoreNextQueryUpdate = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
ui.searchResultsView.initialize(
|
||||||
|
SearchResultsView.Configuration(
|
||||||
|
commonConfiguration = CommonSearchViewConfiguration()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
placeAutocompleteUiAdapter = PlaceAutocompleteUiAdapter(
|
||||||
|
view = ui.searchResultsView,
|
||||||
|
placeAutocomplete = placeAutocomplete
|
||||||
|
)
|
||||||
|
placeAutocompleteUiAdapter.addSearchListener(object : PlaceAutocompleteUiAdapter.SearchListener {
|
||||||
|
|
||||||
|
override fun onSuggestionsShown(suggestions: List<PlaceAutocompleteSuggestion>) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuggestionSelected(suggestion: PlaceAutocompleteSuggestion) {
|
||||||
|
openPlaceCard(suggestion)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPopulateQueryClick(suggestion: PlaceAutocompleteSuggestion) {
|
||||||
|
ui.queryEditText.setText(suggestion.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Exception) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.searchPlaceView.apply {
|
||||||
|
initialize(CommonSearchViewConfiguration())
|
||||||
|
|
||||||
|
isFavoriteButtonVisible = false
|
||||||
|
|
||||||
|
addOnCloseClickListener {
|
||||||
|
hide()
|
||||||
|
closePlaceCard()
|
||||||
|
}
|
||||||
|
|
||||||
|
addOnNavigateClickListener { searchPlace ->
|
||||||
|
mapControl.routeToPoint(searchPlace.coordinate)
|
||||||
|
mapControl.navigationStatusControl.sendEvent(NavigationStatus.STARTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
addOnShareClickListener { _ ->
|
||||||
|
showToast(R.string.not_implemented_yet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.queryEditText.addTextChangedListener(object : TextWatcher {
|
||||||
|
|
||||||
|
override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
|
||||||
|
if (ignoreNextQueryUpdate) {
|
||||||
|
ignoreNextQueryUpdate = false
|
||||||
|
} else {
|
||||||
|
closePlaceCard()
|
||||||
|
}
|
||||||
|
|
||||||
|
mapControl.context.apply {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
placeAutocompleteUiAdapter.search(text.toString())
|
||||||
|
ui.searchResultsView.isVisible = text.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.mapView.gestures.addOnMapLongClickListener {
|
||||||
|
reverseGeocoding(it)
|
||||||
|
return@addOnMapLongClickListener true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPlaceCard(suggestion: PlaceAutocompleteSuggestion) {
|
||||||
|
ignoreNextQueryUpdate = true
|
||||||
|
ui.queryEditText.setText("")
|
||||||
|
|
||||||
|
mapControl.context.apply {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
placeAutocomplete.select(suggestion).onValue { result ->
|
||||||
|
mapMarkersManager.showMarker(result.coordinate)
|
||||||
|
ui.searchPlaceView.open(SearchPlace.createFromPlaceAutocompleteResult(result))
|
||||||
|
ui.queryEditText.hideKeyboard()
|
||||||
|
ui.searchResultsView.isVisible = false
|
||||||
|
}.onError { error ->
|
||||||
|
Log.d(LOG_TAG, "Suggestion selection error", error)
|
||||||
|
showToast(R.string.place_autocomplete_selection_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closePlaceCard() {
|
||||||
|
ui.searchPlaceView.hide()
|
||||||
|
mapMarkersManager.clearMarkers()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reverseGeocoding(point: Point) {
|
||||||
|
val types: List<PlaceAutocompleteType> = when (ui.mapView.mapboxMap.cameraState.zoom) {
|
||||||
|
in 0.0..4.0 -> REGION_LEVEL_TYPES
|
||||||
|
in 4.0..6.0 -> DISTRICT_LEVEL_TYPES
|
||||||
|
in 6.0..12.0 -> LOCALITY_LEVEL_TYPES
|
||||||
|
else -> ALL_TYPES
|
||||||
|
}
|
||||||
|
|
||||||
|
mapControl.context.lifecycleScope.launch {
|
||||||
|
mapControl.context.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
val response = placeAutocomplete.reverse(point, PlaceAutocompleteOptions(types = types))
|
||||||
|
response.onValue { suggestions ->
|
||||||
|
if (suggestions.isEmpty()) {
|
||||||
|
showToast(R.string.place_autocomplete_reverse_geocoding_error_message)
|
||||||
|
} else {
|
||||||
|
openPlaceCard(suggestions.first())
|
||||||
|
}
|
||||||
|
}.onError { error ->
|
||||||
|
Log.d(LOG_TAG, "Reverse geocoding error", error)
|
||||||
|
showToast(R.string.place_autocomplete_reverse_geocoding_error_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNavigationStatusChanged(navigationStatus: NavigationStatus) {
|
||||||
|
when (navigationStatus) {
|
||||||
|
NavigationStatus.IDLE -> {
|
||||||
|
ui.queryEditText.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
NavigationStatus.STARTED -> {
|
||||||
|
mapControl.navigationCamera.requestNavigationCameraToOverview()
|
||||||
|
closePlaceCard()
|
||||||
|
ui.queryEditText.visibility = View.GONE
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showToast(@StringRes resId: Int): Unit =
|
||||||
|
Toast.makeText(mapControl.context, resId, Toast.LENGTH_LONG).show()
|
||||||
|
|
||||||
|
private fun View.hideKeyboard() =
|
||||||
|
(mapControl.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||||
|
.hideSoftInputFromWindow(windowToken, 0)
|
||||||
|
|
||||||
|
private class MapMarkersManager(mapView: MapView) {
|
||||||
|
|
||||||
|
private val mapboxMap = mapView.mapboxMap
|
||||||
|
private val circleAnnotationManager = mapView.annotations.createCircleAnnotationManager(null)
|
||||||
|
private val markers = mutableMapOf<String, Point>()
|
||||||
|
|
||||||
|
fun clearMarkers() {
|
||||||
|
markers.clear()
|
||||||
|
circleAnnotationManager.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMarker(coordinate: Point) {
|
||||||
|
clearMarkers()
|
||||||
|
|
||||||
|
val circleAnnotationOptions: CircleAnnotationOptions = CircleAnnotationOptions()
|
||||||
|
.withPoint(coordinate)
|
||||||
|
.withCircleRadius(8.0)
|
||||||
|
.withCircleColor("#ee4e8b")
|
||||||
|
.withCircleStrokeWidth(2.0)
|
||||||
|
.withCircleStrokeColor("#ffffff")
|
||||||
|
|
||||||
|
val annotation = circleAnnotationManager.create(circleAnnotationOptions)
|
||||||
|
markers[annotation.id] = coordinate
|
||||||
|
|
||||||
|
CameraOptions.Builder()
|
||||||
|
.center(coordinate)
|
||||||
|
.padding(MARKERS_INSETS_OPEN_CARD)
|
||||||
|
.zoom(14.0)
|
||||||
|
.build().also {
|
||||||
|
mapboxMap.setCamera(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val LOG_TAG = "AutocompleteUiActivity"
|
||||||
|
|
||||||
|
val MARKERS_EDGE_OFFSET = dpToPx(64).toDouble()
|
||||||
|
val PLACE_CARD_HEIGHT = dpToPx(300).toDouble()
|
||||||
|
val MARKERS_TOP_OFFSET = dpToPx(88).toDouble()
|
||||||
|
|
||||||
|
val MARKERS_INSETS_OPEN_CARD = EdgeInsets(
|
||||||
|
MARKERS_TOP_OFFSET, MARKERS_EDGE_OFFSET, PLACE_CARD_HEIGHT, MARKERS_EDGE_OFFSET
|
||||||
|
)
|
||||||
|
|
||||||
|
val REGION_LEVEL_TYPES = listOf(
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Country,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Region
|
||||||
|
)
|
||||||
|
|
||||||
|
val DISTRICT_LEVEL_TYPES = REGION_LEVEL_TYPES + listOf(
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Postcode,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.District
|
||||||
|
)
|
||||||
|
|
||||||
|
val LOCALITY_LEVEL_TYPES = DISTRICT_LEVEL_TYPES + listOf(
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Place,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Locality
|
||||||
|
)
|
||||||
|
|
||||||
|
private val ALL_TYPES = listOf(
|
||||||
|
PlaceAutocompleteType.Poi,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Country,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Region,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Postcode,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.District,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Place,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Locality,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Neighborhood,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Street,
|
||||||
|
PlaceAutocompleteType.AdministrativeUnit.Address,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun dpToPx(dp: Int): Int = (dp * Resources.getSystem().displayMetrics.density).toInt()
|
||||||
|
}
|
||||||
|
}
|
8
app/src/main/res/drawable/card_background.xml
Normal file
8
app/src/main/res/drawable/card_background.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<solid android:color="@color/white" />
|
||||||
|
|
||||||
|
</shape>
|
|
@ -4,6 +4,23 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/query"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:autofillHints="@null"
|
||||||
|
android:background="@drawable/card_background"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:hint="@string/place_autocomplete_query_hint"
|
||||||
|
android:inputType="text"
|
||||||
|
android:minHeight="?actionBarSize"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.mapbox.maps.MapView
|
<com.mapbox.maps.MapView
|
||||||
android:id="@+id/mapView"
|
android:id="@+id/mapView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -38,6 +55,39 @@
|
||||||
app:srcCompat="@android:drawable/ic_delete" />
|
app:srcCompat="@android:drawable/ic_delete" />
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<com.mapbox.search.ui.view.SearchResultsView
|
||||||
|
android:id="@+id/searchResults"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="@drawable/card_background"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="22dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/query"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
android:id="@+id/searchContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:elevation="@dimen/search_card_elevation"
|
||||||
|
>
|
||||||
|
|
||||||
|
<com.mapbox.search.ui.view.place.SearchPlaceBottomSheetView
|
||||||
|
android:id="@+id/searchPlaces"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:elevation="@dimen/search_card_elevation"
|
||||||
|
/>
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
<com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
|
<com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
|
||||||
android:id="@+id/maneuverView"
|
android:id="@+id/maneuverView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
4
app/src/main/res/values/dimensions.xml
Normal file
4
app/src/main/res/values/dimensions.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="search_card_elevation">8dp</dimen>
|
||||||
|
</resources>
|
|
@ -1,4 +1,10 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Garmin</string>
|
<string name="app_name">Garmin</string>
|
||||||
<string name="mapbox_access_token">pk.eyJ1IjoibWFwcy16dHNoIiwiYSI6ImNsbDl4YXU4cjA3eW8zcXMzbXdjYjNsN3oifQ.kbDCjthamXvXX_pAdsq3hQ</string>
|
<string name="mapbox_access_token">pk.eyJ1IjoibWFwcy16dHNoIiwiYSI6ImNsbDl4YXU4cjA3eW8zcXMzbXdjYjNsN3oifQ.kbDCjthamXvXX_pAdsq3hQ</string>
|
||||||
|
|
||||||
|
<string name="place_autocomplete_query_hint">Search for places</string>
|
||||||
|
<string name="place_autocomplete_reverse_geocoding_error_message">Unable to locate selected coordinate</string>
|
||||||
|
<string name="place_autocomplete_selection_error">Error happened during request</string>
|
||||||
|
<string name="not_implemented_yet">Not implemented yet</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue