Search with geocode

View on GitHubSample viewer app

Find the location for an address or places of interest near a location or within a specific area.

Screenshot for search with geocode sample

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

  1. Create a LocatorTask using the World GeocodeServer.
  2. Create the GeocodeParameters and set the maxResults and resultAttributeNames to return.
  3. To get suggestion results use LocatorTask.suggest(SearchText).
  4. Perform a locator search LocatorTask.geocode(SearchText, GeocodeParameters).
    • To search in MapView's extent, setGeocodeParameters.searchArea = mapView.getCurrentViewpoint()
  5. Identify a result pin graphic in the graphics overlay at GeocodeResult.displayLocation and add the attributes GeocodeResult.attributes to the graphic
  6. 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

MainActivity.kt
Use dark colors for code blocksCopy
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
/* 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.esri.arcgismaps.sample.searchwithgeocode.databinding.SearchWithGeocodeActivityMainBinding
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: SearchWithGeocodeActivityMainBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.search_with_geocode_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.ACCESS_TOKEN)
        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()) {
                                val 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 and set it to query
                                        val selectedAddress =
                                            selectedRow.getString(selectedCursorIndex)
                                        addressSearchView.setQuery(selectedAddress, false)
                                        // geocode the typed address
                                        geocodeAddress(selectedAddress, false)
                                        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()
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.