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. Click on a map in the list to open it. If a locator task is available, click on the map to place a point. Click it again to reverse geocode the location's address. If transportation networks are available, click the route icon in the top right corner a route will be calculated between geocode locations.
How it works
- Create a 
MobileMapPackagepassing in the path to the constructor. - Get a list model of maps inside the package using the 
mapsproperty. - If the package has a locator, access it using the 
LocatorTaskproperty. - To see if a map contains transportation networks, check each map's 
TransportationNetworksproperty. 
Relevant API
- GeocodeResult
 - MobileMapPackage
 - ReverseGeocodeParameters
 - Route
 - RouteParameters
 - RouteResult
 - RouteTask
 - TransportationNetworkDataset
 
Offline data
Read more about how to set up the sample's offline data here.
| Link | Local Location | 
|---|---|
| Yellowstone mmpk File | <userhome>/ArcGIS/Runtime/Data/mmpk/Yellowstone.mmpk | 
| SanFrancisco mmpk File | <userhome>/ArcGIS/Runtime/Data/mmpk/SanFrancisco.mmpk | 
Tags
disconnected, field mobility, geocode, network, network analysis, offline, routing, search, transportation
Sample Code
// [WriteFile Name=MobileMap_SearchAndRoute, Category=Maps]
// [Legal]
// Copyright 2016 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.
// [Legal]
#ifdef PCH_BUILD
#include "pch.hpp"
#endif // PCH_BUILD
// sample headers
#include "MobileMap_SearchAndRoute.h"
// ArcGIS Maps SDK headers
#include "AttributeListModel.h"
#include "CalloutData.h"
#include "Error.h"
#include "GeocodeResult.h"
#include "Graphic.h"
#include "GraphicListModel.h"
#include "GraphicsOverlay.h"
#include "GraphicsOverlayListModel.h"
#include "IdentifyGraphicsOverlayResult.h"
#include "Item.h"
#include "LocatorTask.h"
#include "Map.h"
#include "MapQuickView.h"
#include "MapTypes.h"
#include "MapViewTypes.h"
#include "MobileMapPackage.h"
#include "PictureMarkerSymbol.h"
#include "Polyline.h"
#include "ReverseGeocodeParameters.h"
#include "Route.h"
#include "RouteParameters.h"
#include "RouteResult.h"
#include "RouteTask.h"
#include "SimpleLineSymbol.h"
#include "SimpleRenderer.h"
#include "Stop.h"
#include "SymbolTypes.h"
#include "TextSymbol.h"
// Qt headers
#include <QDir>
#include <QFile>
#include <QFileInfoList>
#include <QFuture>
#include <QStandardPaths>
#include <QUuid>
#include <QtCore/qglobal.h>
using namespace Esri::ArcGISRuntime;
// helper method to get cross platform data path
namespace
{
QString defaultDataPath()
{
  QString dataPath;
#ifdef Q_OS_IOS
  dataPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
#else
  dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
#endif
  return dataPath;
}
} // namespace
MobileMap_SearchAndRoute::MobileMap_SearchAndRoute(QQuickItem* parent):
  QQuickItem(parent),
  m_selectedMmpkIndex(0),
  m_canRoute(false),
  m_canClear(false),
  m_isGeocodeInProgress(false),
  m_dataPath(defaultDataPath() + "/ArcGIS/Runtime/Data/mmpk"),
  m_fileInfoList(QDir(m_dataPath).entryInfoList())
{
}
MobileMap_SearchAndRoute::~MobileMap_SearchAndRoute() = default;
void MobileMap_SearchAndRoute::init()
{
  qmlRegisterType<MapQuickView>("Esri.Samples", 1, 0, "MapView");
  qmlRegisterType<MobileMap_SearchAndRoute>("Esri.Samples", 1, 0, "MobileMap_SearchAndRouteSample");
  qmlRegisterUncreatableType<CalloutData>("Esri.Samples", 1, 0, "CalloutData", "CalloutData is an uncreatable type");
}
void MobileMap_SearchAndRoute::componentComplete()
{
  QQuickItem::componentComplete();
  // find QML MapView component
  m_mapView = findChild<MapQuickView*>("mapView");
  m_mapView->setWrapAroundMode(WrapAroundMode::Disabled);
  // initialize Callout
  m_mapView->calloutData()->setTitle("Address");
  // set reverse geocoding parameters
  m_reverseGeocodeParameters.setMaxResults(1);
  // identify and create MobileMapPackages using mmpk files in datapath
  createMobileMapPackages(0);
  // create graphics overlays to visually display geocoding and routing results
  m_stopsGraphicsOverlay = new GraphicsOverlay(this);
  m_routeGraphicsOverlay = new GraphicsOverlay(this);
  m_routeGraphicsOverlay->setRenderer(new SimpleRenderer(new SimpleLineSymbol(SimpleLineSymbolStyle::Solid, QColor("#2196F3"), 4, this), this));
  m_mapView->graphicsOverlays()->append(m_routeGraphicsOverlay);
  m_mapView->graphicsOverlays()->append(m_stopsGraphicsOverlay);
  // create a pin symbol
  m_bluePinSymbol = new PictureMarkerSymbol(QUrl("qrc:/Samples/Maps/MobileMap_SearchAndRoute/bluePinSymbol.png"), this);
  m_bluePinSymbol->setHeight(36);
  m_bluePinSymbol->setWidth(36);
  m_bluePinSymbol->setOffsetY(m_bluePinSymbol->height() / 2);
  connectSignals();
}
void MobileMap_SearchAndRoute::createMobileMapPackages(int index)
{
  if (index < m_fileInfoList.length())
  {
    // check if file is a .mmpk file
    if (m_fileInfoList[index].completeSuffix() == "mmpk")
    {
      // create a new MobileMapPackage
      MobileMapPackage* mobileMapPackage = new MobileMapPackage(m_fileInfoList[index].absoluteFilePath(), this);
      // once MMPK is finished loading, add it and its information to lists
      connect(mobileMapPackage, &MobileMapPackage::doneLoading, this, [mobileMapPackage, this](const Error& error)
      {
        if (error.isEmpty())
        {
          // QList of MobileMapPackages
          m_mobileMapPackages.append(mobileMapPackage);
          // QStringList of MobileMapPackage names. Used as a ListModel in QML
          m_mobileMapPackageList << mobileMapPackage->item()->title();
          emit mmpkListChanged();
        }
      });
      // load the new MMPK
      mobileMapPackage->load();
    }
    createMobileMapPackages(++index);
  }
  else
    return;
}
void MobileMap_SearchAndRoute::connectSignals()
{
  connect(m_mapView, &MapQuickView::mouseClicked, this, [this](QMouseEvent& mouseEvent)
  {
    if (!m_currentLocatorTask)
      return;
\
    m_clickedPoint = Point(m_mapView->screenToLocation(mouseEvent.position().x(), mouseEvent.position().y()));
    // determine if user clicked on a graphic
    m_mapView->identifyGraphicsOverlayAsync(m_stopsGraphicsOverlay, mouseEvent.position(), 5, false, 2).then(this,
    [this](const IdentifyGraphicsOverlayResult* identifyResult)
    {
      if (!identifyResult)
        return;
      // get graphics list
      QList<Graphic*> graphics = identifyResult->graphics();
      if (!graphics.isEmpty())
      {
        // use the blue pin graphic instead of text graphic to as calloutData's geoElement
        Graphic* graphic = graphics[0];
        if (graphic->symbol()->symbolType() != SymbolType::PictureMarkerSymbol && graphics.count() > 1)
          graphic = graphics[1];
        m_mapView->calloutData()->setGeoElement(graphic);
        m_mapView->calloutData()->setDetail(graphic->attributes()->attributeValue("AddressLabel").toString());
        m_mapView->calloutData()->setVisible(true);
      }
      // if clicked a point with no graphic on it, reverse geocode
      else
      {
        m_currentLocatorTask->reverseGeocodeWithParametersAsync(m_clickedPoint, m_reverseGeocodeParameters).then(this,
        [this](const QList<GeocodeResult>& geocodeResults)
        {
          // make busy indicator invisible
          m_isGeocodeInProgress = false;
          emit isGeocodeInProgressChanged();
          if (geocodeResults.isEmpty())
            return;
          // create parent for the graphic
          if (!m_stopGraphicParent)
            m_stopGraphicParent = new QObject(this);
          // create a blue pin graphic to display location
          Graphic* bluePinGraphic = new Graphic(geocodeResults[0].displayLocation(), m_bluePinSymbol, m_stopGraphicParent);
          bluePinGraphic->attributes()->insertAttribute("AddressLabel", geocodeResults[0].label());
          m_stopsGraphicsOverlay->graphics()->append(bluePinGraphic);
          // make clear graphics overlay button visible
          m_canClear = true;
          emit canClearChanged();
          // if routing is not enabled in map, return
          if (!m_currentRouteTask)
            return;
          //Set up the Stops
          // create a stop based on added graphic
          m_stops << Stop(geometry_cast<Point>(bluePinGraphic->geometry()));
          // create a Text Symbol to display stop number
          TextSymbol* textSymbol = new TextSymbol(m_stopGraphicParent);
          textSymbol->setText(QString::number(m_stops.count()));
          textSymbol->setColor(QColor("white"));
          textSymbol->setSize(18);
          textSymbol->setOffsetY(m_bluePinSymbol->height() / 2);
          // create a Graphic using the textSymbol
          Graphic* stopNumberGraphic = new Graphic(bluePinGraphic->geometry(), textSymbol, m_stopGraphicParent);
          stopNumberGraphic->setZIndex(bluePinGraphic->zIndex() + 1);
          m_stopsGraphicsOverlay->graphics()->append(stopNumberGraphic);
          if (m_stops.count() > 1)
          {
            m_canRoute = true;
            emit canRouteChanged();
          }
        });
        m_isGeocodeInProgress = true;
        emit isGeocodeInProgressChanged();
      }
    });
  });
}
void MobileMap_SearchAndRoute::resetMapView()
{
  // dismiss mapView controls
  m_canClear = false;
  emit canClearChanged();
  m_canRoute = false;
  emit canRouteChanged();
  // dimiss callout
  m_mapView->calloutData()->setVisible(false);
  // clear the graphics overlays and stops
  m_stopsGraphicsOverlay->graphics()->clear();
  if (m_stopGraphicParent)
  {
    delete m_stopGraphicParent;
    m_stopGraphicParent = nullptr;
  }
  m_routeGraphicsOverlay->graphics()->clear();
  if (m_routeGraphicParent)
  {
    delete m_routeGraphicParent;
    m_routeGraphicParent = nullptr;
  }
  m_stops.clear();
}
void MobileMap_SearchAndRoute::createMapList(int index)
{
  m_mapList.clear();
  m_selectedMmpkIndex = index;
  int counter = 1;
  const auto maps = m_mobileMapPackages[index]->maps();
  for (const Map* map : maps)
  {
    QVariantMap mapList;
    mapList["name"] = map->item()->title() + " " + QString::number(counter);
    mapList["geocoding"] = m_mobileMapPackages[index]->locatorTask() != nullptr;
    mapList["routing"] = map->transportationNetworks().count() > 0;
    m_mapList << mapList;
    ++counter;
  }
  emit mapListChanged();
}
void MobileMap_SearchAndRoute::selectMap(int index)
{
  resetMapView();
  // set the locatorTask
  //! [MobileMap_SearchAndRoute create LocatorTask]
  m_currentLocatorTask = m_mobileMapPackages[m_selectedMmpkIndex]->locatorTask();
  //! [MobileMap_SearchAndRoute create LocatorTask]
  // set the MapView
  m_mapView->setMap(m_mobileMapPackages[m_selectedMmpkIndex]->maps().at(index));
  // create a RouteTask with selected map's transportation network if available
  if (m_mobileMapPackages[m_selectedMmpkIndex]->maps().at(index)->transportationNetworks().count() > 0)
  {
    m_currentRouteTask = new RouteTask(m_mobileMapPackages[m_selectedMmpkIndex]->maps().at(index)->transportationNetworks().at(0), this);
    m_currentRouteTask->load();
    // create default parameters after the RouteTask is loaded
    connect(m_currentRouteTask, &RouteTask::loadStatusChanged, this, [this](LoadStatus loadStatus)
    {
      if (loadStatus == LoadStatus::Loaded)
        m_currentRouteTask->createDefaultParametersAsync().then(this, [this](const RouteParameters& routeParameters)
        {
          m_currentRouteParameters = routeParameters;
        });
    });
  }
  else
    m_currentRouteTask = nullptr;
}
void MobileMap_SearchAndRoute::solveRoute()
{
  // clear previously displayed routes
  m_routeGraphicsOverlay->graphics()->clear();
  if (m_routeGraphicParent)
  {
    delete m_routeGraphicParent;
    m_routeGraphicParent = nullptr;
  }
  // set stops and solve route
  m_currentRouteParameters.setStops(m_stops);
  // create a graphic using the RouteResult
  m_currentRouteTask->solveRouteAsync(m_currentRouteParameters).then(this,
  [this](const RouteResult& routeResult)
  {
    if (!m_routeGraphicParent)
      m_routeGraphicParent = new QObject(this);
    if (!routeResult.isEmpty())
    {
      const auto routes = routeResult.routes();
      Graphic* routeGraphic = new Graphic(routes[0].routeGeometry(), m_routeGraphicParent);
      m_routeGraphicsOverlay->graphics()->append(routeGraphic);
    }
  });
}
QStringList MobileMap_SearchAndRoute::mmpkList() const
{
  return m_mobileMapPackageList;
}
QVariantList MobileMap_SearchAndRoute::mapList() const
{
  return m_mapList;
}
bool MobileMap_SearchAndRoute::isGeocodeInProgress() const
{
  return m_isGeocodeInProgress;
}
bool MobileMap_SearchAndRoute::canRoute() const
{
  return m_canRoute;
}
bool MobileMap_SearchAndRoute::canClear() const
{
  return m_canClear;
}