Use the OfflineMapTask
to take a web map offline, but instead of downloading an online basemap, use one which is already on the device.
Use case
Reasons to use a basemap which is already on the device, rather than downloading:
- You want to limit the total download size.
- You want to be able to share a single set of basemap files between many offline maps.
- You want to use a custom basemap (for example authored in ArcGIS Pro) which is not available online.
- You do not wish to sign into ArcGIS.com in order to download Esri basemaps.
The author of a web map can support the use of basemaps which are already on a device by configuring the web map to specify the name of a suitable basemap file. This could be a basemap which:
- Has been authored in ArcGIS Pro to make use of your organizations custom data.
- Is available as a PortalItem which can be downloaded once and re-used many times.
How to use the sample
- Use the button to start taking the map offline.
- Specify whether to download the basemap or use a basemap already taken offline.
- If you choose to download the online basemap, the offline map will be generated with the same (topographic) basemap as the online web map.
- To download the Esri basemap, you may be prompted to sign in to ArcGIS.com.
- If you choose to use the basemap from the device, the offline map will be generated with the local imagery basemap. The download will be quicker since no tiles are exported or downloaded.
- Since the application is not exporting online ArcGIS Online basemaps you will not need to log-in.
How it works
The sample creates a PortalItem
object using a web map's ID. This portal item is used to initialize an OfflineMapTask
object. When the button is clicked, the sample requests the default parameters for the task, with the selected extent, by calling OfflineMapTask.CreateDefaultGenerateOfflineMapParameters
.
Once the parameters are created, the application checks the GenerateOfflineMapParameters.ReferenceBasemapFilename
property. The author of an online web map can configure this setting to indicate the name of a suitable basemap. In this example, the application checks the local file-system for the suggested "naperville_imagery.tpkx".
If the user chooses to use the basemap on the device, the GenerateOfflineMapParameters.ReferenceBasemapDirectory
is set to the absolute path of the directory which contains the .tpkx file.
A GenerateOfflineMapJob
is created by calling OfflineMapTask.GenerateOfflineMap
passing the parameters and the download location for the offline map.
When the GenerateOfflineMapJob
is started it will check whether GenerateOfflineMapParameters.ReferenceBasemapDirectory
has been set. If this property is set, no online basemap will be downloaded and instead, the mobile map will be created with a reference to the .tpkx on the device.
Relevant API
- GenerateOfflineMapJob
- GenerateOfflineMapParameters
- GenerateOfflineMapResult
- OfflineMapTask
Offline data
This sample uses naperville_imagery.tpkx. It is downloaded from ArcGIS Online automatically before the sample runs.
Tags
basemap, download, local, offline, save, web map
Sample Code
// Copyright 2021 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.
using ArcGIS.Samples.Managers;
using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.Mapping;
using Esri.ArcGISRuntime.Portal;
using Esri.ArcGISRuntime.Symbology;
using Esri.ArcGISRuntime.Tasks;
using Esri.ArcGISRuntime.Tasks.Offline;
using Esri.ArcGISRuntime.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
namespace ArcGIS.WPF.Samples.OfflineBasemapByReference
{
[ArcGIS.Samples.Shared.Attributes.Sample(
name: "Generate offline map with local basemap",
category: "Map",
description: "Use the `OfflineMapTask` to take a web map offline, but instead of downloading an online basemap, use one which is already on the device.",
instructions: "1. Use the button to start taking the map offline.",
tags: new[] { "basemap", "download", "local", "offline", "save", "web map" })]
[ArcGIS.Samples.Shared.Attributes.OfflineData("85282f2aaa2844d8935cdb8722e22a93")]
public partial class OfflineBasemapByReference
{
// The job to generate an offline map.
private GenerateOfflineMapJob _generateOfflineMapJob;
// The extent of the data to take offline.
private readonly Envelope _areaOfInterest = new Envelope(-88.1541, 41.7690, -88.1471, 41.7720, SpatialReferences.Wgs84);
// The ID for a web map item hosted on the server (water network map of Naperville IL).
private const string WebMapId = "acc027394bc84c2fb04d1ed317aac674";
public OfflineBasemapByReference()
{
InitializeComponent();
_ = Initialize();
}
private void ConfigureOfflineJobForBasemap(GenerateOfflineMapParameters parameters)
{
// Don't give the user a choice if there is no basemap specified.
if (string.IsNullOrWhiteSpace(parameters.ReferenceBasemapFilename))
{
return;
}
// Get the path to the basemap directory.
string basemapBasePath = DataManager.GetDataFolder("85282f2aaa2844d8935cdb8722e22a93");
// Get the full path to the basemap by combining the name specified in the web map (ReferenceBasemapFilename)
// with the offline basemap directory.
string basemapFullPath = Path.Combine(basemapBasePath, parameters.ReferenceBasemapFilename);
// If the offline basemap doesn't exist, proceed without it.
if (!File.Exists(basemapFullPath))
{
return;
}
// Get the user's choice.
MessageBoxResult userChoice = MessageBox.Show("Use the offline basemap?", "Basemap choice", MessageBoxButton.YesNo);
// If the user approves, use the offline basemap.
if (userChoice == MessageBoxResult.Yes)
{
parameters.ReferenceBasemapDirectory = basemapBasePath;
}
}
// Note: all code below (except call to ConfigureOfflineJobForBasemap) is identical to code in the Generate offline map sample.
#region Generate offline map
private async Task Initialize()
{
try
{
// Create the ArcGIS Online portal.
ArcGISPortal portal = await ArcGISPortal.CreateAsync();
// Get the Naperville water web map item using its ID.
PortalItem webmapItem = await PortalItem.CreateAsync(portal, WebMapId);
// Create a map from the web map item.
Map onlineMap = new Map(webmapItem);
// Display the map in the MapView.
MyMapView.Map = onlineMap;
// Disable user interactions on the map (no panning or zooming from the initial extent).
MyMapView.InteractionOptions = new MapViewInteractionOptions
{
IsEnabled = false
};
// Create a graphics overlay for the extent graphic and apply a renderer.
SimpleLineSymbol aoiOutlineSymbol = new SimpleLineSymbol(SimpleLineSymbolStyle.Solid, System.Drawing.Color.Red, 3);
GraphicsOverlay extentOverlay = new GraphicsOverlay
{
Renderer = new SimpleRenderer(aoiOutlineSymbol)
};
MyMapView.GraphicsOverlays.Add(extentOverlay);
// Add a graphic to show the area of interest (extent) that will be taken offline.
Graphic aoiGraphic = new Graphic(_areaOfInterest);
extentOverlay.Graphics.Add(aoiGraphic);
// Hide the map loading progress indicator.
LoadingIndicator.Visibility = Visibility.Collapsed;
// When the map view unloads, try to clean up existing output data folders.
MyMapView.Unloaded += (s, e) =>
{
// Find output mobile map folders in the temp directory.
string[] outputFolders = Directory.GetDirectories(Environment.ExpandEnvironmentVariables("%TEMP%"), "NapervilleWaterNetwork*");
// Loop through the folder names and delete them.
foreach (string dir in outputFolders)
{
try
{
// Delete the folder.
Directory.Delete(dir, true);
}
catch (Exception)
{
// Ignore exceptions (files might be locked, for example).
}
}
};
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "Error loading map");
}
}
private async void TakeMapOfflineButton_Click(object sender, RoutedEventArgs e)
{
// Create a new folder for the output mobile map.
string packagePath = Path.Combine(Environment.ExpandEnvironmentVariables("%TEMP%"), @"NapervilleWaterNetwork");
int num = 1;
while (Directory.Exists(packagePath))
{
packagePath = Path.Combine(Environment.ExpandEnvironmentVariables("%TEMP%"), @"NapervilleWaterNetwork" + num.ToString());
num++;
}
// Create the output directory.
Directory.CreateDirectory(packagePath);
try
{
// Show the progress indicator while the job is running.
BusyIndicator.Visibility = Visibility.Visible;
// Create an offline map task with the current (online) map.
OfflineMapTask takeMapOfflineTask = await OfflineMapTask.CreateAsync(MyMapView.Map);
// Create the default parameters for the task, pass in the area of interest.
GenerateOfflineMapParameters parameters = await takeMapOfflineTask.CreateDefaultGenerateOfflineMapParametersAsync(_areaOfInterest);
// Configure basemap settings for the job.
ConfigureOfflineJobForBasemap(parameters);
// Create the job with the parameters and output location.
_generateOfflineMapJob = takeMapOfflineTask.GenerateOfflineMap(parameters, packagePath);
// Handle the progress changed event for the job.
_generateOfflineMapJob.ProgressChanged += OfflineMapJob_ProgressChanged;
// Await the job to generate geodatabases, export tile packages, and create the mobile map package.
GenerateOfflineMapResult results = await _generateOfflineMapJob.GetResultAsync();
// Check for job failure (writing the output was denied, e.g.).
if (_generateOfflineMapJob.Status != JobStatus.Succeeded)
{
MessageBox.Show("Generate offline map package failed.", "Job status");
BusyIndicator.Visibility = Visibility.Collapsed;
}
// Check for errors with individual layers.
if (results.LayerErrors.Any())
{
// Build a string to show all layer errors.
System.Text.StringBuilder errorBuilder = new System.Text.StringBuilder();
foreach (KeyValuePair<Layer, Exception> layerError in results.LayerErrors)
{
errorBuilder.AppendLine($"{layerError.Key.Id} : {layerError.Value.Message}");
}
// Show layer errors.
string errorText = errorBuilder.ToString();
MessageBox.Show(errorText, "Layer errors");
}
// Display the offline map.
MyMapView.Map = results.OfflineMap;
// Apply the original viewpoint for the offline map.
MyMapView.SetViewpoint(new Viewpoint(_areaOfInterest));
// Enable map interaction so the user can explore the offline data.
MyMapView.InteractionOptions.IsEnabled = true;
// Show a message that the map is offline.
MessageArea.Visibility = Visibility.Visible;
// Hide the controls.
TakeOfflineArea.Visibility = Visibility.Collapsed;
}
catch (TaskCanceledException)
{
// Generate offline map task was canceled.
MessageBox.Show("Taking map offline was canceled");
}
catch (Exception ex)
{
// Exception while taking the map offline.
MessageBox.Show(ex.Message, "Offline map error");
}
finally
{
// Hide the activity indicator when the job is done.
BusyIndicator.Visibility = Visibility.Collapsed;
}
}
// Show changes in job progress.
private void OfflineMapJob_ProgressChanged(object sender, EventArgs e)
{
// Get the job.
GenerateOfflineMapJob job = sender as GenerateOfflineMapJob;
// Dispatch to the UI thread.
Dispatcher.Invoke(() =>
{
// Show the percent complete and update the progress bar.
Percentage.Text = job.Progress > 0 ? job.Progress.ToString() + " %" : string.Empty;
ProgressBar.Value = job.Progress;
});
}
private void CancelJobButton_Click(object sender, RoutedEventArgs e)
{
// The user canceled the job.
_generateOfflineMapJob.CancelAsync();
}
#endregion Generate offline map
}
}