Search with geocode
Find the location for an address or places of interest near a location or within a specific area.
Use case
You can input an address into the app's search bar and zoom to the address location. If you do not know the specific address, you can get suggestions and locations for places of interest (POIs) with a natural language query for what the place has ("food"), the type of place ("gym"), or the generic place name ("Coffee"), rather than the specific address. Additionally, you can filter the results to a specific area.
How to use the sample
Enter an address and optionally choose from the list of suggestions to show its location as a pin graphic. Tap on a result pin to display the address. Toggle the switch to search within the MapView's extent. Use it to query again for the currently viewed area on the map.
How it works
- Create a
LocatorTask
using the World GeocodeServer. - Create the
GeocodeParameters
and set themaxResults
andresultAttributeNames
to return. - To get suggestion results use
LocatorTask.suggest(SearchText)
. - Perform a locator search
LocatorTask.geocode(SearchText, GeocodeParameters)
.- To search in MapView's extent, set
GeocodeParameters.searchArea = mapView.getCurrentViewpoint()
- To search in MapView's extent, set
- Identify a result pin graphic in the graphics overlay at
GeocodeResult.displayLocation
and add the attributesGeocodeResult.attributes
to the graphic - Use
Graphic.attributes
to retrieve the address attributes of the graphic to display on map.
Relevant API
- GeocodeParameters
- GeocodeResult
- LocatorTask
- SearchSuggestion
- SuggestResult
Additional information
This sample uses the World Geocoding Service. For more information, see Geocoding service from ArcGIS Developer website.
Tags
address, businesses, geocode, locations, locator, places of interest, POI, point of interest, search, suggestions
Sample Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
/* Copyright 2022 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.esri.arcgismaps.sample.searchwithgeocode
import android.content.Context
import android.database.MatrixCursor
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.provider.BaseColumns
import android.text.SpannableStringBuilder
import android.util.Log
import android.view.Menu
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.cursoradapter.widget.SimpleCursorAdapter
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.geometry.Geometry
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.ViewpointType
import com.arcgismaps.mapping.symbology.PictureMarkerSymbol
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.ScreenCoordinate
import com.arcgismaps.tasks.geocode.GeocodeParameters
import com.arcgismaps.tasks.geocode.GeocodeResult
import com.arcgismaps.tasks.geocode.LocatorTask
import com.arcgismaps.tasks.geocode.SuggestResult
import com.esri.arcgismaps.sample.searchwithgeocode.databinding.ActivityMainBinding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
// set up data binding for the activity
private val activityMainBinding: ActivityMainBinding by lazy {
DataBindingUtil.setContentView(this, R.layout.activity_main)
}
private val mapView by lazy {
activityMainBinding.mapView
}
private val addressTextView: TextView by lazy {
activityMainBinding.addressTextView
}
private val extentSwitch: SwitchMaterial by lazy {
activityMainBinding.extentSwitch
}
// create a locator task from an online service
private val locatorTask: LocatorTask = LocatorTask(
"https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer"
)
// geocode parameters used to perform a search
private val addressGeocodeParameters: GeocodeParameters = GeocodeParameters().apply {
// get all attributes names for the geocode results
resultAttributeNames.addAll(listOf("PlaceName", "Place_addr"))
}
// create a new Graphics Overlay
private val graphicsOverlay: GraphicsOverlay = GraphicsOverlay()
// instance of the map pin symbol
private var pinSourceSymbol: PictureMarkerSymbol? = null
// will search in the map view's viewpoint extent if enabled
private var isSearchInExtent = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// authentication with an API key or named user is
// required to access basemaps and other location services
ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.API_KEY)
lifecycle.addObserver(mapView)
mapView.apply {
// set the map to be displayed in the MapView
map = ArcGISMap(BasemapStyle.ArcGISStreets)
// set map initial viewpoint
map?.initialViewpoint = Viewpoint(40.0, -100.0, 100000000.0)
// define the graphics overlay and add it to the map view
graphicsOverlays.add(graphicsOverlay)
// set an on touch listener on the map view
lifecycleScope.launch {
onSingleTapConfirmed.collect { tapEvent ->
// identify the graphic at the tapped coordinate
val tappedGraphic = identifyGraphic(tapEvent.screenCoordinate)
if (tappedGraphic != null) {
// show the address of the identified graphic
showAddressForGraphic(tappedGraphic)
}
}
}
}
// once the map has loaded successfully, set up address finding UI
lifecycleScope.launch {
// load the map then set up UI
mapView.map?.load()?.onSuccess {
// create the pin symbol
pinSourceSymbol = createPinSymbol()
}?.onFailure {
showError(it.message.toString())
}
}
// set the switch to update the isSearchInExtent value
extentSwitch.setOnCheckedChangeListener { _, isChecked -> isSearchInExtent = isChecked }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu, menu)
val search = menu.findItem(R.id.appSearchBar)
// set up address search view and listeners
setupAddressSearchView(search.actionView as SearchView)
return super.onCreateOptionsMenu(menu)
}
/**
* Sets up the address SearchView and uses MatrixCursor to
* show suggestions to the user as text is entered.
*/
private fun setupAddressSearchView(addressSearchView: SearchView) {
addressSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(address: String): Boolean {
// geocode the typed address, search within map's viewpoint as keyword was submitted
geocodeAddress(address, true)
addressSearchView.clearAndHideKeyboard()
return true
}
override fun onQueryTextChange(newText: String): Boolean {
// if the newText string isn't empty, get suggestions from the locator task
if (newText.isNotEmpty()) {
lifecycleScope.launch {
locatorTask.suggest(newText).onSuccess { suggestResults ->
// create a SimpleCursorAdapter and assign it to the suggestion adapter of the SearchView
val simpleCursorAdapter = createSimpleCursorAdapter()
addressSearchView.suggestionsAdapter = simpleCursorAdapter
// add each address suggestion to a new row
for ((key, result) in suggestResults.withIndex()) {
var suggestionCursor = simpleCursorAdapter.cursor as MatrixCursor
suggestionCursor.addRow(arrayOf<Any>(key, result.label))
}
// notify the adapter when the data updates, so the view can refresh itself
simpleCursorAdapter.notifyDataSetChanged()
// handle an address suggestion being chosen
addressSearchView.setOnSuggestionListener(object :
SearchView.OnSuggestionListener {
override fun onSuggestionSelect(position: Int): Boolean {
return false
}
override fun onSuggestionClick(position: Int): Boolean {
// get the selected row
(simpleCursorAdapter.getItem(position) as? MatrixCursor)?.let { selectedRow ->
// get the row's index
val selectedCursorIndex =
selectedRow.getColumnIndex("address")
// get the string from the row at index
val selectedAddress =
selectedRow.getString(selectedCursorIndex)
// geocode the typed address
geocodeAddress(selectedAddress, false)
addressSearchView.isIconified = true
addressSearchView.clearAndHideKeyboard()
}
return true
}
})
}.onFailure {
showError("Geocode suggestion error: ${it.message.toString()}")
}
}
}
return true
}
})
}
/**
* Creates and returns a SimpleCursorAdapter.
*/
private fun createSimpleCursorAdapter(): SimpleCursorAdapter {
// set up parameters for searching with MatrixCursor
val columnNames = arrayOf(BaseColumns._ID, "address")
val suggestionsCursor = MatrixCursor(columnNames)
// column names for the adapter to look at when mapping data
val cols = arrayOf("address")
// ids that show where data should be assigned in the layout
val to = intArrayOf(R.id.suggestion_address)
// define SimpleCursorAdapter
return SimpleCursorAdapter(
this@MainActivity,
R.layout.suggestion, suggestionsCursor, cols, to, 0
)
}
/**
* Geocode an [address] passed in by the user.
*/
private fun geocodeAddress(address: String, multipleResults: Boolean) = lifecycleScope.launch {
// clear graphics on map before displaying search results
graphicsOverlay.graphics.clear()
// search the map view's extent if enabled
if (isSearchInExtent)
addressGeocodeParameters.searchArea =
mapView.getCurrentViewpoint(ViewpointType.BoundingGeometry)?.targetGeometry
else
addressGeocodeParameters.searchArea = null
// if locator task needs to find multiple results,
// set maxResults to default to `6`.
addressGeocodeParameters.maxResults = if (multipleResults) 6 else 1
// load the locator task
locatorTask.load().getOrThrow()
// run the locatorTask geocode task, passing in the address
val geocodeResults = locatorTask.geocode(address, addressGeocodeParameters).getOrThrow()
// no address found in geocode so return
when {
geocodeResults.isEmpty() && isSearchInExtent -> {
showError("Address not found in map's extent")
return@launch
}
geocodeResults.isEmpty() && !isSearchInExtent -> {
showError("No address found for $address")
return@launch
}
// address found in geocode
else -> displaySearchResultOnMap(geocodeResults)
}
}
/**
* Turns a list of [geocodeResultList] into a point markers and adds it to the graphic overlay of the map.
*/
private fun displaySearchResultOnMap(geocodeResultList: List<GeocodeResult>) {
// clear graphics overlay of existing graphics
graphicsOverlay.graphics.clear()
// create graphic object for each resulting location
geocodeResultList.forEach { geocodeResult ->
val resultLocationGraphic = Graphic(
geocodeResult.displayLocation,
geocodeResult.attributes, pinSourceSymbol
)
// add graphic to location layer
graphicsOverlay.graphics.add(resultLocationGraphic)
}
when (geocodeResultList.size) {
// if there is only one result, display location's address
1 -> {
val addressAttributes = geocodeResultList[0].attributes
val addressString = SpannableStringBuilder()
.append("Selected address\n")
.bold { append("${addressAttributes["PlaceName"]} ${addressAttributes["Place_addr"]}") }
addressTextView.text = addressString
}
// if there are multiple results, display tap pin message
else -> addressTextView.text = getString(R.string.tap_on_pin_to_select_address)
}
// get the envelop to set the viewpoint
val envelope = graphicsOverlay.extent ?: return showError("Geocode result extent is null")
// animate viewpoint to geocode result's extent
lifecycleScope.launch {
mapView.setViewpointGeometry(envelope, 25.0)
}
}
/**
* Identifies the tapped graphic at the [screenCoordinate] and shows it's address.
*/
private suspend fun identifyGraphic(screenCoordinate: ScreenCoordinate): Graphic? {
// from the graphics overlay, get the graphics near the tapped location
val identifyGraphicsOverlayResult = mapView.identifyGraphicsOverlay(
graphicsOverlay,
screenCoordinate,
10.0,
false
).getOrElse { throwable ->
showError("Error with identifyGraphicsOverlay: ${throwable.message.toString()}")
return null
}
// if not graphic selected, return
if (identifyGraphicsOverlayResult.graphics.isEmpty()) {
return null
}
// get the first graphic identified
return identifyGraphicsOverlayResult.graphics[0]
}
/**
* Creates a picture marker symbol from the pin icon, and sets it to half of its original size.
*/
private suspend fun createPinSymbol(): PictureMarkerSymbol {
val pinDrawable = ContextCompat.getDrawable(this, R.drawable.pin) as BitmapDrawable
val pinSymbol = PictureMarkerSymbol.createWithImage(pinDrawable)
pinSymbol.load().getOrThrow()
pinSymbol.width = 19f
pinSymbol.height = 72f
return pinSymbol
}
/**
* Display the address for the tapped [identifiedGraphic] using the attribute values
*/
private suspend fun showAddressForGraphic(identifiedGraphic: Graphic) {
// get the non null value of the geometry
val pinGeometry: Geometry = identifiedGraphic.geometry
?: return showError("Error retrieving geometry for tapped graphic")
// set the viewpoint to the pin location
mapView.apply {
setViewpointGeometry(pinGeometry.extent)
setViewpointScale(10e3)
}
// set the address text
val addressAttributes = identifiedGraphic.attributes
val addressString = SpannableStringBuilder()
.append("Selected address\n")
.bold { append("${addressAttributes["PlaceName"]} ${addressAttributes["Place_addr"]}") }
addressTextView.text = addressString
}
fun SearchView.clearAndHideKeyboard() {
// clear the searched text from the view
this.clearFocus()
// close the keyboard once search is complete
val inputManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(windowToken, 0)
}
private fun showError(message: String) {
Log.e(localClassName, message)
Snackbar.make(mapView, message, Snackbar.LENGTH_SHORT).show()
}
}