feat: Search basics

This commit is contained in:
Piotr Dec 2024-08-07 19:57:38 +02:00
parent ca7d418726
commit 65cdeb17d0
Signed by: stawros
GPG key ID: F89F27AD8F881A91
9 changed files with 304 additions and 5 deletions

View file

@ -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'

View file

@ -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() {

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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")
}
}

View file

@ -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

View 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()
}
}

View file

@ -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"

View file

@ -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>