Offline geocode

View inJavaKotlinView on GitHubSample viewer app

Geocode addresses to locations and reverse geocode locations to addresses offline.

Image of offline geocode

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

Select an address from the drop-down list to Geocode the address and view the result on the map. Tap the location you want to reverse geocode. Select the pin to highlight the PictureMarkerSymbol (i.e. single tap on the pin) and then tap-hold and drag on the map to get real-time geocoding.

How it works

  1. Use the path of a .loc file to create a LocatorTask object.
  2. Set up GeocodeParameters and call GeocodeAsync to get geocode results.

Relevant API

  • GeocodeParameters
  • GeocodeResult
  • LocatorTask
  • ReverseGeocodeParameters

Offline Data

  1. Download the data San Diego Streets Tile Package and San Diego Offline Locator from ArcGIS Online.
  2. Extract the contents of the downloaded zip file to disk.
  3. Open your command prompt and navigate to the folder where you extracted the contents of the data from step 1.
  4. 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

MainActivity.java
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
/* Copyright 2018 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.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.SearchView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

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.Callout;
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.IdentifyGraphicsOverlayResult;
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 java.util.List;
import java.util.concurrent.ExecutionException;

public class MainActivity extends AppCompatActivity {

  private static final String TAG = MainActivity.class.getSimpleName();

  private GraphicsOverlay mGraphicsOverlay;
  private GeocodeParameters mGeocodeParameters;
  private MapView mMapView;
  private LocatorTask mLocatorTask;
  private ReverseGeocodeParameters mReverseGeocodeParameters;
  private SearchView mSearchView;
  private SimpleMarkerSymbol mPointSymbol;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // get a reference to the map view
    mMapView = findViewById(R.id.mapView);
    // define a map
    ArcGISMap map = new ArcGISMap();
    // set the map to the map view
    mMapView.setMap(map);
    // add a graphics overlay to the map view
    mGraphicsOverlay = new GraphicsOverlay();
    mMapView.getGraphicsOverlays().add(mGraphicsOverlay);
    // create a point symbol for showing the address location
    mPointSymbol = new SimpleMarkerSymbol(SimpleMarkerSymbol.Style.CIRCLE, Color.RED, 20.0f);
    // add a touch listener to the map view
    mMapView.setOnTouchListener(new CustomMapViewOnTouchListener(this, mMapView));

    // load the tile cache from local storage
    TileCache tileCache = new TileCache(getExternalFilesDir(null) + getString(R.string.san_diego_tpkx));
    // use the tile cache extent to set the view point
    tileCache.addDoneLoadingListener(() -> mMapView.setViewpoint(new Viewpoint(tileCache.getFullExtent())));
    // create a tiled layer and add it to as the base map
    ArcGISTiledLayer tiledLayer = new ArcGISTiledLayer(tileCache);
    mMapView.getMap().setBasemap(new Basemap(tiledLayer));
    // create geocode parameters
    mGeocodeParameters = new GeocodeParameters();
    mGeocodeParameters.getResultAttributeNames().add("*");
    mGeocodeParameters.setMaxResults(1);
    // create reverse geocode parameters
    mReverseGeocodeParameters = new ReverseGeocodeParameters();
    mReverseGeocodeParameters.getResultAttributeNames().add("*");
    mReverseGeocodeParameters.setOutputSpatialReference(mMapView.getMap().getSpatialReference());
    mReverseGeocodeParameters.setMaxResults(1);
    // load the locator task from external storage
    mLocatorTask = new LocatorTask(
        getExternalFilesDir(null) + getResources().getString(R.string.san_diego_loc));
    mLocatorTask.loadAsync();

    mSearchView = findViewById(R.id.searchView);
    mSearchView.setIconifiedByDefault(true);
    mSearchView.setQueryHint(getString(R.string.search_hint));
    mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
      @Override
      public boolean onQueryTextSubmit(String query) {
        geoCodeTypedAddress(query);
        mSearchView.clearFocus();
        return true;
      }

      @Override
      public boolean onQueryTextChange(String newText) {
        return false;
      }
    });
    // create an array adapter using the string array and a default spinner layout
    final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>(this,
        android.R.layout.simple_spinner_dropdown_item) {
      @Override
      public int getCount() {
        return super.getCount() - 1;
      }
    };
    // specify the layout to use when the list of choices appears
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    adapter.addAll(getResources().getStringArray(R.array.suggestion_items));
    Spinner spinner = findViewById(R.id.spinner);
    // Apply the adapter to the spinner
    spinner.setAdapter(adapter);
    spinner.setSelection(adapter.getCount());
    spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
      @Override
      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        if (position == adapter.getCount()) {
          mSearchView.clearFocus();
        } else {
          mSearchView.setQuery(getResources().getStringArray(R.array.suggestion_items)[position], false);
          geoCodeTypedAddress(getResources().getStringArray(R.array.suggestion_items)[position]);
          mSearchView.setIconified(false);
          mSearchView.clearFocus();
        }
      }

      @Override
      public void onNothingSelected(AdapterView<?> parent) {
      }
    });
  }

  /**
   * Use the locator task to geocode the the given address.
   *
   * @param address as a string
   */
  private void geoCodeTypedAddress(final String address) {
    // Execute async task to find the address
    mLocatorTask.addDoneLoadingListener(() -> {
      if (mLocatorTask.getLoadStatus() == LoadStatus.LOADED) {
        // get a list of geocode results for the given address
        ListenableFuture<List<GeocodeResult>> geocodeFuture = mLocatorTask.geocodeAsync(address, mGeocodeParameters);
        geocodeFuture.addDoneListener(() -> {
          try {
            // get the geocode results
            List<GeocodeResult> geocodeResults = geocodeFuture.get();
            if (!geocodeResults.isEmpty()) {
              // get the first result
              GeocodeResult geocodeResult = geocodeResults.get(0);
              displayGeocodeResult(geocodeResult.getDisplayLocation(), geocodeResult.getLabel());
            } else {
              Toast.makeText(this, "No location found for: " + address, Toast.LENGTH_LONG).show();
            }
          } catch (InterruptedException | ExecutionException e) {
            String error = "Error getting geocode result: " + e.getMessage();
            Toast.makeText(this, error, Toast.LENGTH_LONG).show();
            Log.e(TAG, error);
          }
        });
      } else {
        String error = "Error loading locator task: " + mLocatorTask.getLoadError().getMessage();
        Toast.makeText(this, error, Toast.LENGTH_LONG).show();
        Log.e(TAG, error);
      }
    });
  }

  /**
   * Uses the locator task to reverse geocode for the given point.
   *
   * @param point on which to perform the reverse geocode
   */
  private void reverseGeocode(Point point) {
    final ListenableFuture<List<GeocodeResult>> results = mLocatorTask.reverseGeocodeAsync(point, mReverseGeocodeParameters);
    try {
      List<GeocodeResult> geocodeResults = results.get();
      if (!geocodeResults.isEmpty()) {
        // get the top result
        GeocodeResult geocode = geocodeResults.get(0);
        String detail;
        // attributes from a click-based search
        String street = geocode.getAttributes().get("StAddr").toString();
        String city = geocode.getAttributes().get("City").toString();
        String region = geocode.getAttributes().get("Region").toString();
        String postCode = geocode.getAttributes().get("Postal").toString();
        detail = city + ", " + region + ", " + postCode;
        String address = street + ", " + detail;
        displayGeocodeResult(point, address);
      }
    } catch (ExecutionException | InterruptedException e) {
      String error = "Error getting geocode results: " + e.getMessage();
      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 void displayGeocodeResult(Point resultPoint, CharSequence address) {
    // dismiss the callout if showing
    if (mMapView.getCallout().isShowing()) {
      mMapView.getCallout().dismiss();
    }
    // remove any previous graphics/search results
    mGraphicsOverlay.getGraphics().clear();
    // create graphic object for resulting location
    Graphic pointGraphic = new Graphic(resultPoint, mPointSymbol);
    // add graphic to location layer
    mGraphicsOverlay.getGraphics().add(pointGraphic);
    // Zoom map to geocode result location
    mMapView.setViewpointAsync(new Viewpoint(resultPoint, 8000), 3);
    showCallout(resultPoint, address);
  }

  /**
   * Show a callout at the given point with the given text.
   *
   * @param point to define callout location
   * @param text to define callout content
   */
  private void showCallout(Point point, CharSequence text) {
    Callout callout = mMapView.getCallout();
    TextView calloutTextView = new TextView(this);
    calloutTextView.setText(text);
    callout.setLocation(point);
    callout.setContent(calloutTextView);
    callout.show();
  }

  /**
   * Define a listener to handle drag events.
   */
  private class DragTouchListener extends DefaultMapViewOnTouchListener {

    DragTouchListener(Context context, MapView mapView) {
      super(context, mapView);
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
      switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
          final int pointerIndex = event.getActionIndex();
          final float x = event.getX(pointerIndex);
          final float y = event.getY(pointerIndex);
          android.graphics.Point screenPoint = new android.graphics.Point(Math.round(x), Math.round(y));
          final Point singleTapPoint = mMapView.screenToLocation(screenPoint);
          reverseGeocode(singleTapPoint);
          break;
        case MotionEvent.ACTION_UP:
          if (!mGraphicsOverlay.getGraphics().isEmpty()) {
            mGraphicsOverlay.getGraphics().get(0).setSelected(false);
            mMapView.setOnTouchListener(new CustomMapViewOnTouchListener(getApplicationContext(), mMapView));
          }
          break;
        default:
          return true;
      }
      return true;
    }
  }

  /**
   * Define a listener to handle long press (reverse geocode the point) and single taps (select the tapped graphic).
   */
  private class CustomMapViewOnTouchListener extends DefaultMapViewOnTouchListener {

    CustomMapViewOnTouchListener(Context context, MapView mapView) {
      super(context, mapView);
    }

    @Override
    public void onLongPress(MotionEvent event) {
      android.graphics.Point screenPoint = new android.graphics.Point(Math.round(event.getX()), Math.round(event.getY()));
      Point mapPoint = mMapView.screenToLocation(screenPoint);
      reverseGeocode(mapPoint);
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent event) {
      if (!mGraphicsOverlay.getGraphics().isEmpty()) {
        if (mGraphicsOverlay.getGraphics().get(0).isSelected()) {
          mGraphicsOverlay.getGraphics().get(0).setSelected(false);
        }
      }
      // get the screen point where user tapped
      final android.graphics.Point screenPoint = new android.graphics.Point((int) event.getX(),
          (int) event.getY());
      // identify graphics on the graphics overlay
      final ListenableFuture<IdentifyGraphicsOverlayResult> identifyGraphic = mMapView
          .identifyGraphicsOverlayAsync(mGraphicsOverlay, screenPoint, 10.0, false, 1);
      identifyGraphic.addDoneListener(() -> {
        try {
          IdentifyGraphicsOverlayResult grOverlayResult = identifyGraphic.get();
          // get the list of graphics returned by identify
          List<Graphic> graphic = grOverlayResult.getGraphics();
          // if identified graphic is not empty, start DragTouchListener
          if (!graphic.isEmpty()) {
            graphic.get(0).setSelected(true);
            Toast.makeText(MainActivity.this, getString(R.string.reverse_geocode_message), Toast.LENGTH_SHORT).show();
            mMapView.setOnTouchListener(new DragTouchListener(getApplicationContext(), mMapView));
          }
        } catch (InterruptedException | ExecutionException e) {
          String error = "Error identifying graphic: " + e.getMessage();
          Toast.makeText(MainActivity.this, error, Toast.LENGTH_LONG).show();
          Log.e(TAG, error);
        }
      });
      return super.onSingleTapConfirmed(event);
    }
  }

  @Override
  protected void onPause() {
    mMapView.pause();
    super.onPause();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mMapView.resume();
  }

  @Override
  protected void onDestroy() {
    mMapView.dispose();
    super.onDestroy();
  }
}

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