Geocode addresses to locations and reverse geocode locations to addresses offline.
Use case
You can use an address locator file to geocode addresses and locations. For example, you could provide offline geocoding capabilities to field workers repairing critical infrastructure in a disaster when network availability is limited.
How to use the sample
Search for an address from the search bar or select a suggestion to Geocode
the address and view the result on the map. Tap the location you want to reverse geocode, or double-tap and drag on the map to get real-time geocoding.
How it works
- Use the path of a .loc file to create a
LocatorTask
object. - Set up
GeocodeParameters
and callGeocodeAsync
to get geocode results.
Relevant API
- GeocodeParameters
- GeocodeResult
- LocatorTask
- ReverseGeocodeParameters
Offline Data
- Download the data San Diego Streets Tile Package and San Diego Offline Locator from ArcGIS Online.
- Extract the contents of the downloaded zip file to disk.
- Open your command prompt and navigate to the folder where you extracted the contents of the data from step 1.
- Push the data into the scoped storage of the sample app:
adb push streetmap_SD.tpkx /Android/data/com.esri.arcgisruntime.sample.offlinegeocode/files/streetmap_SD.tpkx
adb push san-diego-eagle-locator/. /Android/data/com.esri.arcgisruntime.sample.offlinegeocode/files
Tags
geocode, geocoder, locator, offline, package, query, search
Sample Code
/*
* Copyright 2020 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.arcgisruntime.sample.offlinegeocode
import android.database.MatrixCursor
import android.graphics.Color
import android.os.Bundle
import android.provider.BaseColumns
import android.util.Log
import android.view.MotionEvent
import android.widget.AutoCompleteTextView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.cursoradapter.widget.SimpleCursorAdapter
import com.esri.arcgisruntime.concurrent.ListenableFuture
import com.esri.arcgisruntime.data.TileCache
import com.esri.arcgisruntime.geometry.Point
import com.esri.arcgisruntime.layers.ArcGISTiledLayer
import com.esri.arcgisruntime.loadable.LoadStatus
import com.esri.arcgisruntime.mapping.ArcGISMap
import com.esri.arcgisruntime.mapping.Basemap
import com.esri.arcgisruntime.mapping.Viewpoint
import com.esri.arcgisruntime.mapping.view.DefaultMapViewOnTouchListener
import com.esri.arcgisruntime.mapping.view.Graphic
import com.esri.arcgisruntime.mapping.view.GraphicsOverlay
import com.esri.arcgisruntime.mapping.view.MapView
import com.esri.arcgisruntime.symbology.SimpleMarkerSymbol
import com.esri.arcgisruntime.tasks.geocode.GeocodeParameters
import com.esri.arcgisruntime.tasks.geocode.GeocodeResult
import com.esri.arcgisruntime.tasks.geocode.LocatorTask
import com.esri.arcgisruntime.tasks.geocode.ReverseGeocodeParameters
import com.esri.arcgisruntime.sample.offlinegeocode.databinding.ActivityMainBinding
import kotlin.math.roundToInt
class MainActivity : AppCompatActivity() {
private val TAG = MainActivity::class.java.simpleName
private val activityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private val searchView: SearchView by lazy {
activityMainBinding.searchView
}
private val mapView: MapView by lazy {
activityMainBinding.mapView
}
private val geocodeParameters: GeocodeParameters by lazy {
GeocodeParameters().apply {
// get all attributes
resultAttributeNames.add("*")
// get only the closest result
maxResults = 1
}
}
private val reverseGeocodeParameters: ReverseGeocodeParameters by lazy {
ReverseGeocodeParameters().apply {
// get all attributes
resultAttributeNames.add("*")
// use the map's spatial reference
outputSpatialReference = mapView.map.spatialReference
// get only the closest result
maxResults = 1
}
}
private val locatorTask: LocatorTask by lazy {
LocatorTask(
getExternalFilesDir(null)?.path + resources.getString(R.string.san_diego_loc)
)
}
// create a point symbol for showing the address location
private val pointSymbol = SimpleMarkerSymbol(SimpleMarkerSymbol.Style.CIRCLE, Color.RED, 20.0f)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(activityMainBinding.root)
// load the tile cache from local storage
val tileCache =
TileCache(getExternalFilesDir(null)?.path + getString(R.string.san_diego_tpkx))
// use the tile cache extent to set the view point
tileCache.addDoneLoadingListener { mapView.setViewpoint(Viewpoint(tileCache.fullExtent)) }
// create a tiled layer from the tile cache
val tiledLayer = ArcGISTiledLayer(tileCache)
// set up the map view
mapView.apply {
// create a map with the tiled layer base map
map = ArcGISMap(Basemap(tiledLayer))
// add a graphics overlay to the map view
graphicsOverlays.add(GraphicsOverlay())
// add a touch listener to the map view
onTouchListener = object : DefaultMapViewOnTouchListener(this@MainActivity, mapView) {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
val screenPoint = android.graphics.Point(e.x.roundToInt(), e.y.toInt())
reverseGeocode(mapView.screenToLocation(screenPoint))
return true
}
override fun onDoubleTouchDrag(e: MotionEvent): Boolean {
return onSingleTapConfirmed(e)
}
}
}
// load the locator task from external storage
locatorTask.loadAsync()
locatorTask.addDoneLoadingListener { setupAddressSearchView() }
}
/**
* Use the locator task to geocode the given address.
*
* @param address as a string to geocode
*/
private fun geocode(address: String) {
// execute async task to find the address
locatorTask.addDoneLoadingListener {
if (locatorTask.loadStatus != LoadStatus.LOADED) {
val error =
"Error loading locator task: " + locatorTask.loadError.message
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
Log.e(TAG, error)
return@addDoneLoadingListener
}
// get a list of geocode results for the given address
val geocodeFuture: ListenableFuture<List<GeocodeResult>> =
locatorTask.geocodeAsync(address, geocodeParameters)
geocodeFuture.addDoneListener {
try {
// get the geocode results
val geocodeResults = geocodeFuture.get()
if (geocodeResults.isEmpty()) {
Toast.makeText(this, "No location found for: $address", Toast.LENGTH_LONG)
.show()
return@addDoneListener
}
// get the first result
val geocodeResult = geocodeResults[0]
displayGeocodeResult(geocodeResult.displayLocation, geocodeResult.label)
} catch (e: Exception) {
val error = "Error getting geocode result: " + e.message
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
Log.e(TAG, error)
}
}
}
}
/**
* Uses the locator task to reverse geocode the given point.
*
* @param point on which to perform the reverse geocode
*/
private fun reverseGeocode(point: Point) {
val results = locatorTask.reverseGeocodeAsync(point, reverseGeocodeParameters)
try {
val geocodeResults = results.get()
if (geocodeResults.isEmpty()) {
Toast.makeText(this, "No addresses found at that location!", Toast.LENGTH_LONG)
.show()
return
}
// get the top result
val geocode = geocodeResults[0]
// attributes from a click-based search
val street = geocode.attributes["StAddr"].toString()
val city = geocode.attributes["City"].toString()
val state = geocode.attributes["Region"].toString()
val zip = geocode.attributes["Postal"].toString()
val detail = "$city, $state $zip"
val address = "$street, $detail"
displayGeocodeResult(point, address)
} catch (e: Exception) {
val error = "Error getting geocode results: " + e.message
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
Log.e(TAG, error)
}
}
/**
* Draw a point and open a callout showing geocode results on map.
*
* @param resultPoint geometry to show where the geocode result is
* @param address to display in the associated callout
*/
private fun displayGeocodeResult(resultPoint: Point, address: CharSequence) {
// dismiss the callout if showing
if (mapView.callout.isShowing) {
mapView.callout.dismiss()
}
val graphicsOverlay = mapView.graphicsOverlays[0]
// remove any previous graphics/search results
graphicsOverlay.graphics.clear()
// create graphic object for resulting location and add it to the ographics overlay
graphicsOverlay.graphics.add(Graphic(resultPoint, pointSymbol))
// zoom map to geocode result location
mapView.setViewpointAsync(Viewpoint(resultPoint, 8000.0), 3f)
showCallout(resultPoint, address)
}
/**
* Show a callout at the given point with the given text.
*
* @param point to define callout location
* @param calloutText to define callout content
*/
private fun showCallout(point: Point, calloutText: CharSequence) {
val calloutTextView = TextView(this).apply {
text = calloutText
}
mapView.callout.apply {
location = point
content = calloutTextView
}
mapView.callout.show()
}
/**
* Sets up the address SearchView and uses MatrixCursor to show suggestions to the user.
*/
private fun setupAddressSearchView() {
// get the list of pre-made suggestions
val suggestions = resources.getStringArray(R.array.suggestion_items)
// set up parameters for searching with MatrixCursor
val columnNames = arrayOf(BaseColumns._ID, "address")
val suggestionsCursor = MatrixCursor(columnNames)
// add each address suggestion to a new row
suggestions.forEachIndexed { i, s -> suggestionsCursor.addRow(arrayOf(i, s)) }
// create the adapter for the search view's suggestions
searchView.apply {
suggestionsAdapter = SimpleCursorAdapter(
this@MainActivity,
R.layout.offline_geocode_suggestion,
suggestionsCursor,
arrayOf("address"),
intArrayOf(R.id.suggestion_address),
0
)
// show the suggestions as soon as the user opens the search view
findViewById<AutoCompleteTextView>(R.id.search_src_text).threshold = 0
// geocode the searched address on submit
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(address: String): Boolean {
geocode(address)
searchView.clearFocus()
return true
}
override fun onQueryTextChange(newText: String?) = true
})
// geocode a suggestions when selected
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
override fun onSuggestionSelect(position: Int) = true
override fun onSuggestionClick(position: Int): Boolean {
geocode(suggestions[position])
return true
}
})
}
}
override fun onPause() {
super.onPause()
mapView.pause()
}
override fun onResume() {
super.onResume()
mapView.resume()
}
override fun onDestroy() {
super.onDestroy()
mapView.dispose()
}
}