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 {
|
||||
mapboxVersion = '3.2.0'
|
||||
searchApiVersion = '2.3.0'
|
||||
searchApiVersion = '2.3.1'
|
||||
searchNativeVersion = '2.2.1'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'com.mapbox.maps:android:11.5.1'
|
||||
|
||||
implementation "com.mapbox.navigationcore:navigation:$mapboxVersion"
|
||||
implementation "com.mapbox.navigationcore:ui-maps:$mapboxVersion"
|
||||
implementation "com.mapbox.navigationcore:voice:$mapboxVersion"
|
||||
implementation "com.mapbox.navigationcore:tripdata:$mapboxVersion"
|
||||
implementation "com.mapbox.navigationcore:ui-components:$mapboxVersion"
|
||||
|
||||
implementation "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.appcompat:appcompat:1.7.0'
|
||||
|
|
|
@ -27,6 +27,7 @@ import eu.ztsh.garmin.mapbox.MapControl
|
|||
import eu.ztsh.garmin.util.PermissionsHelper
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
|
|
|
@ -56,4 +56,7 @@ class UI(b: ActivityMainBinding) {
|
|||
val routeOverview = b.routeOverview
|
||||
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 routeProgressObserver: RouteProgressObserver
|
||||
private lateinit var voiceInstructionsObserver: VoiceInstructionsObserver
|
||||
private val searchControl = SearchControl(this, ui)
|
||||
private val navigationStateListener = NavigationStateListener()
|
||||
|
||||
fun init() {
|
||||
viewportDataSource = MapboxNavigationViewportDataSource(ui.mapView.mapboxMap)
|
||||
|
@ -133,6 +135,7 @@ class MapControl(
|
|||
mapboxNavigation.registerLocationObserver(locationObserver)
|
||||
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
|
||||
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
|
||||
mapboxNavigation.registerNavigationSessionStateObserver(navigationStateListener)
|
||||
|
||||
replay.onAttached(mapboxNavigation)
|
||||
}
|
||||
|
@ -142,6 +145,8 @@ class MapControl(
|
|||
mapboxNavigation.unregisterLocationObserver(locationObserver)
|
||||
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
|
||||
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
|
||||
mapboxNavigation.unregisterNavigationSessionStateObserver(navigationStateListener)
|
||||
|
||||
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)
|
||||
|
||||
// add long click listener that search for a route to the clicked destination
|
||||
ui.mapView.gestures.addOnMapLongClickListener { point ->
|
||||
findRoute(point)
|
||||
true
|
||||
}
|
||||
// ui.mapView.gestures.addOnMapLongClickListener { point ->
|
||||
// findRoute(point)
|
||||
// true
|
||||
// }
|
||||
}
|
||||
|
||||
// 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:autofillHints="@null"
|
||||
android:background="@drawable/card_background"
|
||||
android:textColor="@color/black"
|
||||
android:elevation="4dp"
|
||||
android:hint="@string/place_autocomplete_query_hint"
|
||||
android:inputType="text"
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
<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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue