Display maps and use locators to enable search and routing offline using a Mobile Map Package.
      
   
    
Use case
Mobile map packages make it easy to transmit and store the necessary components for an offline map experience including: transportation networks (for routing/navigation), locators (address search, forward and reverse geocoding), and maps.
A field worker might download a mobile map package to support their operations while working offline.
How to use the sample
A list of maps from a mobile map package will be displayed. If the map contains transportation networks, the list item will have a navigation icon. Tap on a map in the list to open it. If a locator task is available, tap on the map to reverse geocode the location's address. If transportation networks are available, a route will be calculated between geocode locations.
How it works
- Create a MobileMapPackageusingMobileMapPackage(path).loadAsync().
- Get a list of maps inside the package using the mobileMapPackage.getMaps().
- If the package has a locator, access it using mobileMapPackage.getLocatorTask().
- To see if a map contains transportation networks, check map.getTransportationNetworks().
Relevant API
- GeocodeResult
- MobileMapPackage
- ReverseGeocodeParameters
- Route
- RouteParameters
- RouteResult
- RouteTask
Offline Data
- Download the data from ArcGIS Online.
- 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 SanFrancisco.mmpk /Android/data/com.esri.arcgisruntime.sample.mobilemapsearchandroute/files/SanFrancisco.mmpk
Tags
disconnected, field mobility, geocode, network, network analysis, offline, routing, search, transportation
Sample Code
/*
 * Copyright 2017 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.mobilemapsearchandroute;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.esri.arcgisruntime.concurrent.ListenableFuture;
import com.esri.arcgisruntime.geometry.Point;
import com.esri.arcgisruntime.loadable.LoadStatus;
import com.esri.arcgisruntime.mapping.ArcGISMap;
import com.esri.arcgisruntime.mapping.MobileMapPackage;
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.CompositeSymbol;
import com.esri.arcgisruntime.symbology.SimpleLineSymbol;
import com.esri.arcgisruntime.symbology.SimpleMarkerSymbol;
import com.esri.arcgisruntime.symbology.Symbol;
import com.esri.arcgisruntime.symbology.TextSymbol;
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.tasks.networkanalysis.Route;
import com.esri.arcgisruntime.tasks.networkanalysis.RouteParameters;
import com.esri.arcgisruntime.tasks.networkanalysis.RouteResult;
import com.esri.arcgisruntime.tasks.networkanalysis.RouteTask;
import com.esri.arcgisruntime.tasks.networkanalysis.Stop;
/**
 * This class demonstrates offline functionality through the use of a mobile map package (mmpk).
 * <p>
 * This (main) activity handles:
 * loading of map package,
 * loading of maps and map previews from map packages,
 * searching (ie reverse geocoding), and
 * routing.
 */
public class MainActivity extends AppCompatActivity {
  private static final String TAG = MainActivity.class.getSimpleName();
  private GraphicsOverlay mMarkerGraphicsOverlay;
  private GraphicsOverlay mRouteGraphicsOverlay;
  private RouteTask mRouteTask;
  private RouteParameters mRouteParameters;
  private final ArrayList<MapPreview> mMapPreviews = new ArrayList<>();
  private MobileMapPackage mMobileMapPackage;
  private MapView mMapView;
  private String mMMPkTitle;
  private LocatorTask mLocatorTask;
  private Callout mCallout;
  private ReverseGeocodeParameters mReverseGeocodeParameters;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // initialize reverse geocode params
    mReverseGeocodeParameters = new ReverseGeocodeParameters();
    mReverseGeocodeParameters.setMaxResults(1);
    mReverseGeocodeParameters.getResultAttributeNames().add("*");
    // retrieve the MapView from layout
    mMapView = findViewById(R.id.mapView);
    // add route and marker overlays to map view
    mMarkerGraphicsOverlay = new GraphicsOverlay();
    mRouteGraphicsOverlay = new GraphicsOverlay();
    mMapView.getGraphicsOverlays().add(mRouteGraphicsOverlay);
    mMapView.getGraphicsOverlays().add(mMarkerGraphicsOverlay);
    // add the map from the mobile map package to the MapView
    loadMobileMapPackage(getExternalFilesDir(null) + getString(R.string.san_francisco_mmpk));
    mMapView.setOnTouchListener(new DefaultMapViewOnTouchListener(this, mMapView) {
      @Override
      public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
        // get the point that was clicked and convert it to a point in map coordinates
        android.graphics.Point screenPoint = new android.graphics.Point(Math.round(motionEvent.getX()), Math.round(motionEvent.getY()));
        // create a map point from screen point
        Point mapPoint = mMapView.screenToLocation(screenPoint);
        geoView(screenPoint, mapPoint);
        return true;
      }
    });
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // create button in action bar to allow user to access MapChooserActivity
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.map_preview_list, menu);
    return super.onCreateOptionsMenu(menu);
  }
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    final int MAP_CHOSEN_RESULT = 1;
    Intent mapChooserIntent = new Intent(getApplicationContext(), MapChooserActivity.class);
    // pass the list of mapPreviews
    mapChooserIntent.putExtra("map_previews", mMapPreviews);
    // pass the mobile map package title
    mapChooserIntent.putExtra("MMPk_title", mMMPkTitle);
    // start MapChooserActivity to determine user's chosen map number
    startActivityForResult(mapChooserIntent, MAP_CHOSEN_RESULT);
    return super.onOptionsItemSelected(item);
  }
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (data != null) {
      // get the map number chosen in MapChooserActivity and load that map
      int mapNum = data.getIntExtra("map_num", -1);
      loadMap(mapNum);
      // dismiss any callout boxes
      if (mCallout != null) {
        mCallout.dismiss();
      }
      // clear any existing graphics
      mMarkerGraphicsOverlay.getGraphics().clear();
      mRouteGraphicsOverlay.getGraphics().clear();
    }
    super.onActivityResult(requestCode, resultCode, data);
  }
  /**
   * Loads a mobile map package and map previews.
   *
   * @param path to location of mobile map package on device
   */
  private void loadMobileMapPackage(String path) {
    // create the mobile map package
    mMobileMapPackage = new MobileMapPackage(path);
    // load the mobile map package asynchronously
    mMobileMapPackage.loadAsync();
    // add done listener which will load when package has maps
    mMobileMapPackage.addDoneLoadingListener(() -> {
      // check load status and that the mobile map package has maps
      if (mMobileMapPackage.getLoadStatus() == LoadStatus.LOADED && !mMobileMapPackage.getMaps().isEmpty()) {
        mLocatorTask = mMobileMapPackage.getLocatorTask();
        // default to display of first map in package
        loadMap(0);
        loadMapPreviews();
      } else {
        String error = "Mobile map package failed to load: " + mMobileMapPackage.getLoadError().getMessage();
        Log.e(TAG, error);
        Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
      }
    });
  }
  /**
   * Loads map from the mobile map package for a given index.
   *
   * @param mapNum index of map in mobile map package
   */
  private void loadMap(int mapNum) {
    ArcGISMap map = mMobileMapPackage.getMaps().get(mapNum);
    // check if the map contains transport networks
    if (map.getTransportationNetworks().isEmpty()) {
      // only allow routing on map with transport networks
      mRouteTask = null;
    } else {
      mRouteTask = new RouteTask(this, map.getTransportationNetworks().get(0));
      try {
        mRouteParameters = mRouteTask.createDefaultParametersAsync().get();
      } catch (ExecutionException | InterruptedException e) {
        String error = "Error creating route task default parameters: " + e.getMessage();
        Log.e(TAG, error);
        Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
      }
    }
    mMapView.setMap(map);
  }
  /**
   * Generates and populates the map preview models from information in the mobile map package.
   */
  private void loadMapPreviews() {
    // set mobile map package title
    mMMPkTitle = mMobileMapPackage.getItem().getTitle();
    // for each map in the mobile map package, pull out relevant thumbnail information
    for (int i = 0; i < mMobileMapPackage.getMaps().size(); i++) {
      ArcGISMap currMap = mMobileMapPackage.getMaps().get(i);
      final MapPreview mapPreview = new MapPreview();
      // set map number
      mapPreview.setMapNum(i);
      // set map title. If null use the index of the list of maps to name each map Map #
      if (currMap.getItem() != null && currMap.getItem().getTitle() != null) {
        mapPreview.setTitle(currMap.getItem().getTitle());
      } else {
        mapPreview.setTitle("Map " + i);
      }
      // set map description. If null use package description instead
      if (currMap.getItem() != null && currMap.getItem().getDescription() != null) {
        mapPreview.setDesc(currMap.getItem().getDescription());
      } else {
        mapPreview.setDesc(mMobileMapPackage.getItem().getDescription());
      }
      // check if map has transport data
      if (!currMap.getTransportationNetworks().isEmpty()) {
        mapPreview.setTransportNetwork(true);
      }
      // check if map has geocoding data
      if (mMobileMapPackage.getLocatorTask() != null) {
        mapPreview.setGeocoding(true);
      }
      // set map preview thumbnail
      final ListenableFuture<byte[]> thumbnailAsync;
      if (currMap.getItem() != null && currMap.getItem().fetchThumbnailAsync() != null) {
        thumbnailAsync = currMap.getItem().fetchThumbnailAsync();
      } else {
        thumbnailAsync = mMobileMapPackage.getItem().fetchThumbnailAsync();
      }
      thumbnailAsync.addDoneListener(() -> {
        if (thumbnailAsync.isDone()) {
          try {
            mapPreview.setThumbnailByteStream(thumbnailAsync.get());
            mMapPreviews.add(mapPreview);
          } catch (InterruptedException | ExecutionException e) {
            String error = "Error getting thumbnail: " + e.getMessage();
            Log.e(TAG, error);
            Toast.makeText(this, error, Toast.LENGTH_LONG).show();
          }
        }
      });
    }
  }
  /**
   * Defines a graphic symbol which represents geocoded locations.
   *
   * @return the stop graphic
   */
  private SimpleMarkerSymbol simpleSymbolForStopGraphic() {
    SimpleMarkerSymbol simpleMarkerSymbol = new SimpleMarkerSymbol(
        SimpleMarkerSymbol.Style.CIRCLE, Color.RED, 12);
    simpleMarkerSymbol.setLeaderOffsetY(5);
    return simpleMarkerSymbol;
  }
  /**
   * Defines a composite symbol consisting of the SimpleMarkerSymbol and a text symbol
   * representing the index of a stop in a route.
   *
   * @param simpleMarkerSymbol a SimpleMarkerSymbol which represents the background of the
   *                           composite symbol
   * @param index              number which corresponds to the stop number in a route
   * @return the composite symbol
   */
  private CompositeSymbol compositeSymbolForStopGraphic(SimpleMarkerSymbol simpleMarkerSymbol, Integer index) {
    TextSymbol textSymbol = new TextSymbol(12, index.toString(), Color.BLACK,
        TextSymbol.HorizontalAlignment.CENTER, TextSymbol.VerticalAlignment.MIDDLE);
    List<Symbol> compositeSymbolList = new ArrayList<>();
    compositeSymbolList.addAll(Arrays.asList(simpleMarkerSymbol, textSymbol));
    return new CompositeSymbol(compositeSymbolList);
  }
  /**
   * For a given point, returns a graphic.
   *
   * @param point           map point
   * @param isIndexRequired true if used in a route
   * @param index           stop number in a route
   * @return a Graphic at point with either a simple or composite symbol
   */
  private Graphic graphicForPoint(Point point, boolean isIndexRequired, Integer index) {
    // make symbol composite if an index is required
    Symbol symbol;
    if (isIndexRequired && index != null) {
      symbol = compositeSymbolForStopGraphic(simpleSymbolForStopGraphic(), index);
    } else {
      symbol = simpleSymbolForStopGraphic();
    }
    return new Graphic(point, symbol);
  }
  /**
   * Shows the callout for a given graphic.
   *
   * @param graphic     the graphic selected by the user
   * @param tapLocation the location selected at a Point
   */
  private void showCalloutForGraphic(Graphic graphic, Point tapLocation) {
    TextView calloutTextView = (TextView) getLayoutInflater().inflate(R.layout.callout, null);
    calloutTextView.setText(graphic.getAttributes().get("Match_addr").toString());
    mCallout = mMapView.getCallout();
    mCallout.setLocation(tapLocation);
    mCallout.setContent(calloutTextView);
    mCallout.show();
  }
  /**
   * Adds a graphic at a given point to GraphicsOverlay in the MapView. If RouteTask is not null
   * get index for stop symbol. If identifyGraphicsOverlayAsync returns no graphics, call
   * reverseGeocode and route, otherwise just call reverseGeocode.
   *
   * @param screenPoint point on the screen which the user selected
   * @param mapPoint    point on the map which the user selected
   */
  private void geoView(android.graphics.Point screenPoint, final Point mapPoint) {
    if (mRouteTask != null || mLocatorTask != null) {
      if (mRouteTask == null) {
        mMarkerGraphicsOverlay.getGraphics().clear();
      }
      final ListenableFuture<IdentifyGraphicsOverlayResult> identifyGraphicsResult =
          mMapView.identifyGraphicsOverlayAsync(mMarkerGraphicsOverlay, screenPoint, 12, false);
      identifyGraphicsResult.addDoneListener(() -> {
        try {
          Graphic graphic;
          if (identifyGraphicsResult.isDone() && identifyGraphicsResult.get().getGraphics().isEmpty()) {
            if (mRouteTask != null) {
              int index = mMarkerGraphicsOverlay.getGraphics().size() + 1;
              graphic = graphicForPoint(mapPoint, true, index);
            } else {
              graphic = graphicForPoint(mapPoint, false, null);
            }
            mMarkerGraphicsOverlay.getGraphics().add(graphic);
            reverseGeocode(mapPoint, graphic);
            route();
          } else if (identifyGraphicsResult.isDone()) {
            // if graphic exists within screenPoint tolerance, show callout information of clicked graphic
            reverseGeocode(mapPoint, identifyGraphicsResult.get().getGraphics().get(0));
          }
        } catch (Exception e) {
          String error = "Error getting identify graphics result: " + e.getMessage();
          Log.e(TAG, error);
          Toast.makeText(this, error, Toast.LENGTH_LONG).show();
        }
      });
    }
  }
  /**
   * Calls reverseGeocode on a Locator Task and, if there is a result, passes the result to a
   * method which shows callouts.
   *
   * @param point   user generated map point
   * @param graphic used for marking the point on which the user touched
   */
  private void reverseGeocode(final Point point, final Graphic graphic) {
    if (mLocatorTask != null) {
      final ListenableFuture<List<GeocodeResult>> results =
          mLocatorTask.reverseGeocodeAsync(point, mReverseGeocodeParameters);
      results.addDoneListener(() -> {
        try {
          List<GeocodeResult> geocodeResult = results.get();
          if (geocodeResult.isEmpty()) {
            // no result was found
            mMapView.getCallout().dismiss();
          } else {
            graphic.getAttributes().put("Match_addr", geocodeResult.get(0).getLabel());
            showCalloutForGraphic(graphic, point);
          }
        } catch (InterruptedException | ExecutionException e) {
          String error = "Error getting geocode result: " + e.getMessage();
          Log.e(TAG, error);
          Toast.makeText(this, error, Toast.LENGTH_LONG).show();
        }
      });
    }
  }
  /**
   * Uses the last two markers drawn to calculate a route between them.
   */
  private void route() {
    if (mMarkerGraphicsOverlay.getGraphics().size() > 1 && mRouteParameters != null) {
      // create stops for last and second to last graphic
      int size = mMarkerGraphicsOverlay.getGraphics().size();
      List<Graphic> graphics = new ArrayList<>();
      Graphic lastGraphic = mMarkerGraphicsOverlay.getGraphics().get(size - 1);
      graphics.add(lastGraphic);
      Graphic secondLastGraphic = mMarkerGraphicsOverlay.getGraphics().get(size - 2);
      graphics.add(secondLastGraphic);
      // add stops to the parameters
      mRouteParameters.setStops(stopsForGraphics(graphics));
      final ListenableFuture<RouteResult> routeResult = mRouteTask.solveRouteAsync(mRouteParameters);
      routeResult.addDoneListener(() -> {
        try {
          Route route = routeResult.get().getRoutes().get(0);
          Graphic routeGraphic = new Graphic(route.getRouteGeometry(),
              new SimpleLineSymbol(
                  SimpleLineSymbol.Style.SOLID, Color.BLUE, 5.0f));
          mRouteGraphicsOverlay.getGraphics().add(routeGraphic);
        } catch (InterruptedException | ExecutionException e) {
          String error = "Error getting route result: " + e.getMessage();
          Log.e(TAG, error);
          Toast.makeText(this, error, Toast.LENGTH_LONG).show();
          // if routing is interrupted, remove last graphic
          mMarkerGraphicsOverlay.getGraphics().remove(mMarkerGraphicsOverlay.getGraphics().size() - 1);
        }
      });
    }
  }
  /**
   * Converts a given list of graphics into a list of stops.
   *
   * @param graphics to be converted to stops
   * @return a list of stops
   */
  private List<Stop> stopsForGraphics(List<Graphic> graphics) {
    List<Stop> stops = new ArrayList<>();
    for (Graphic graphic : graphics) {
      Stop stop = new Stop((Point) graphic.getGeometry());
      stops.add(stop);
    }
    return stops;
  }
  @Override
  protected void onPause() {
    mMapView.pause();
    super.onPause();
  }
  @Override
  protected void onResume() {
    super.onResume();
    mMapView.resume();
  }
  @Override
  protected void onDestroy() {
    mMapView.dispose();
    super.onDestroy();
  }
}