feat: Search basics
This commit is contained in:
parent
ca7d418726
commit
65cdeb17d0
9 changed files with 304 additions and 5 deletions
|
@ -40,20 +40,26 @@ android {
|
||||||
}
|
}
|
||||||
ext {
|
ext {
|
||||||
mapboxVersion = '3.2.0'
|
mapboxVersion = '3.2.0'
|
||||||
searchApiVersion = '2.3.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:autofill:$searchApiVersion"
|
||||||
implementation "com.mapbox.search:place-autocomplete:$searchApiVersion"
|
implementation "com.mapbox.search:place-autocomplete:$searchApiVersion"
|
||||||
implementation "com.mapbox.search:mapbox-search-android:$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-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'
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -58,6 +58,8 @@ 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)
|
||||||
|
private val navigationStateListener = NavigationStateListener()
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
viewportDataSource = MapboxNavigationViewportDataSource(ui.mapView.mapboxMap)
|
viewportDataSource = MapboxNavigationViewportDataSource(ui.mapView.mapboxMap)
|
||||||
|
@ -133,6 +135,7 @@ class MapControl(
|
||||||
mapboxNavigation.registerLocationObserver(locationObserver)
|
mapboxNavigation.registerLocationObserver(locationObserver)
|
||||||
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
|
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
|
||||||
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
|
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
|
||||||
|
mapboxNavigation.registerNavigationSessionStateObserver(navigationStateListener)
|
||||||
|
|
||||||
replay.onAttached(mapboxNavigation)
|
replay.onAttached(mapboxNavigation)
|
||||||
}
|
}
|
||||||
|
@ -142,6 +145,8 @@ class MapControl(
|
||||||
mapboxNavigation.unregisterLocationObserver(locationObserver)
|
mapboxNavigation.unregisterLocationObserver(locationObserver)
|
||||||
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
|
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
|
||||||
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
|
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
|
||||||
|
mapboxNavigation.unregisterNavigationSessionStateObserver(navigationStateListener)
|
||||||
|
|
||||||
replay.onDetached(mapboxNavigation)
|
replay.onDetached(mapboxNavigation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package eu.ztsh.garmin.mapbox
|
||||||
|
|
||||||
|
import com.mapbox.navigation.core.trip.session.NavigationSessionState
|
||||||
|
import com.mapbox.navigation.core.trip.session.NavigationSessionStateObserver
|
||||||
|
|
||||||
|
class NavigationStateListener: NavigationSessionStateObserver {
|
||||||
|
|
||||||
|
override fun onNavigationSessionStateChanged(navigationSession: NavigationSessionState) {
|
||||||
|
// TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -116,10 +116,10 @@ class RouteControl(private val mapControl: MapControl, ui: UI, private val conte
|
||||||
routeLineView.initializeLayers(it)
|
routeLineView.initializeLayers(it)
|
||||||
|
|
||||||
// add long click listener that search for a route to the clicked destination
|
// add long click listener that search for a route to the clicked destination
|
||||||
ui.mapView.gestures.addOnMapLongClickListener { point ->
|
// ui.mapView.gestures.addOnMapLongClickListener { point ->
|
||||||
findRoute(point)
|
// findRoute(point)
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize view interactions
|
// initialize view interactions
|
||||||
|
|
270
app/src/main/java/eu/ztsh/garmin/mapbox/SearchControl.kt
Normal file
270
app/src/main/java/eu/ztsh/garmin/mapbox/SearchControl.kt
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
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) {
|
||||||
|
|
||||||
|
// 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 ->
|
||||||
|
showToast(R.string.not_implemented_yet)
|
||||||
|
// startActivity(geoIntent(searchPlace.coordinate))
|
||||||
|
}
|
||||||
|
|
||||||
|
addOnShareClickListener { searchPlace ->
|
||||||
|
showToast(R.string.not_implemented_yet)
|
||||||
|
// startActivity(shareIntent(searchPlace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 PERMISSIONS_REQUEST_LOCATION = 0
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@
|
||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
android:autofillHints="@null"
|
android:autofillHints="@null"
|
||||||
android:background="@drawable/card_background"
|
android:background="@drawable/card_background"
|
||||||
|
android:textColor="@color/black"
|
||||||
android:elevation="4dp"
|
android:elevation="4dp"
|
||||||
android:hint="@string/place_autocomplete_query_hint"
|
android:hint="@string/place_autocomplete_query_hint"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
|
|
|
@ -5,5 +5,6 @@
|
||||||
<string name="place_autocomplete_query_hint">Search for places</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_reverse_geocoding_error_message">Unable to locate selected coordinate</string>
|
||||||
<string name="place_autocomplete_selection_error">Error happened during request</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