Unfortunately, the latest SDK lacks examples on caching tiles from a service. How do I use ExportTileCache on a service?
A complete example would be much appreciated.
This is the map I want to cache:
http://www.arcgis.com/home/item.html?id=226d23f076da478bba4589e7eae95952
From what I've read and understood, these are the steps I followed to use ExportTileCache:
1. Register app online
2. Setup AuthenticationManager with Server Info (client id, client secret, oauth2 settings)
3. register ServerInfo
4. Setup export cache parameters
5. Call ExportTileCache on the service
Upon calling ExportTileCache on the service I get the error 'Token required'. Shouldn't ExportTileCache initiate a challenge procedure from the service and in doing so get a token, all handled by the AuthenticationManager?
I am also unsure which URL should be used where. I assume the first URL(SERVICE_OFFLINE_URL) specified here is the URL that I should pass as the ServerUri to ServerInfo, and the fully qualified basemap url(BASEMAP_OFFLINE_URL) passed to ExportTileCache.CreateAsync. Am I correct?
private const string SERVICE_OFFLINE_URL = "https://tiledbasemaps.arcgis.com/arcgis/rest";
private const string OTHER_URL = "https://www.arcgis.com/sharing/rest";
private const string BASEMAP_OFFLINE_URL = "https://tiledbasemaps.arcgis.com/arcgis/rest/services/World_Imagery/MapServer";
private const string OAUTH2_URN = "urn:ietf:wg:oauth:2.0:oob";
private async void CreateTileCacheAsync()
{
Envelope extent =
new Envelope(
new MapPoint(-25.6129589203346, 27.9394285297344, SpatialReferences.Wgs84),
new MapPoint(-26.1950346130634, 29.2006589984844, SpatialReferences.Wgs84));
Authentication.Initialize(CLIENT_ID, CLIENT_SECRET, SERVICE_OFFLINE_URL, OAUTH2_URN);
var t = await ExportTileCacheTask.CreateAsync(new Uri(BASEMAP_OFFLINE_URL), creds);
var cacheParams = await t.CreateDefaultExportTileCacheParametersAsync(extent, 8000, 2000);
var outFolder = System.AppDomain.CurrentDomain.BaseDirectory;
var job = t.ExportTileCache(cacheParams, outFolder + "asdf.tpk");
var result = await job.GetResultAsync();
Debug.WriteLine("Tile Cache created at " + result.Path);
Debug.WriteLine("format: " + result.CacheStorageFormat);
}
Authentication code:
The ChallengeHandler and OAuthAuthorizeHandler are taken from the sample code in the runtime samples Security section.
public static class Authentication
{
public static void Initialize(string clientId, string clientSecret, String serverUrl, String redirectUrl)
{
ClientId = clientId;
ClientSecret = clientSecret;
ServerUrl = serverUrl;
RedirectUrl = redirectUrl;
UpdateAuthenticationManager();
}
private static string ClientId { get; set; }
private static string ClientSecret { get; set; }
private static string ServerUrl { get; set; }
private static string RedirectUrl { get; set; }
private static void UpdateAuthenticationManager()
{
Esri.ArcGISRuntime.Security.ServerInfo serverInfo = new ServerInfo
{
ServerUri = new Uri(ServerUrl),
OAuthClientInfo = new OAuthClientInfo
{
ClientId = ClientId,
RedirectUri = new Uri(RedirectUrl)
}
};
if (!string.IsNullOrEmpty(ClientSecret))
{
serverInfo.TokenAuthenticationType = TokenAuthenticationType.OAuthAuthorizationCode;
serverInfo.OAuthClientInfo.ClientSecret = ClientSecret;
serverInfo.TokenServiceUri = new Uri(TOKEN_SERVICE_URL);
}
else
{
serverInfo.TokenAuthenticationType = TokenAuthenticationType.OAuthImplicit;
}
AuthenticationManager.Current.RegisterServer(serverInfo);
AuthenticationManager.Current.OAuthAuthorizeHandler = new OAuthAuthorize();
AuthenticationManager.Current.ChallengeHandler = new ChallengeHandler(CreateCredentialAsync);
}
// ...
public static async Task<Credential> CreateCredentialAsync(CredentialRequestInfo info)
{
OAuthTokenCredential credential = null;
try
{
// Create generate token options if necessary
if (info.GenerateTokenOptions == null)
info.GenerateTokenOptions = new GenerateTokenOptions();
// AuthenticationManager will handle challenging the user for credentials
credential = await AuthenticationManager.Current.GenerateCredentialAsync(
info.ServiceUri,
info.GenerateTokenOptions)
as OAuthTokenCredential;
}
catch (Exception ex)
{
throw (ex);
}
return credential;
}
Here's some code which works for me. Hopefully you can piece it together and it works for you. Hit me up again if not.
/// <summary>
/// Gets or sets the current basemap URL.
/// </summary>
/// <value>The current basemap URL.</value>
public static string CurrentBasemapUrl {
get {
switch (CurrentBasemapName.Trim ().ToLower ()) {
case "imagery":
return "https://tiledbasemaps.arcgis.com/arcgis/rest/services/World_Imagery/MapServer";
case "streets":
return "https://tiledbasemaps.arcgis.com/arcgis/rest/services/World_Street_Map/MapServer";
case "topo":
return "https://tiledbasemaps.arcgis.com/arcgis/rest/services/World_Topo_Map/MapServer";
default:
return "https://tiledbasemaps.arcgis.com/arcgis/rest/services/World_Imagery/MapServer";
}
}
}
/// <summary>
/// The export tile cache.
/// </summary>
private static ExportTileCacheTask _exportTileTask = null;
/// <summary>
/// The export tile parameters.
/// </summary>
private static ExportTileCacheParameters _tileParameters = null;
/// <summary>
/// Setups the export tile cache.
/// </summary>
/// <returns>The export tile cache.</returns>
/// <param name="mapExtent">Map extent.</param>
/// <param name="maxLOD">Max LOD.</param>
private static async Task SetupExportTileCacheAsync(Envelope mapExtent, int maxLOD)
{
try {
//Generate the token options and credential
var options = new Esri.ArcGISRuntime.Security.GenerateTokenOptions()
{
Referer = new Uri(StorageManager.CurrentBasemapUrl)
};
var user = await ConfigurationManager.GetArcGISUserNameAsync ();
var pass = await ConfigurationManager.GetArcGISPasswordAsync ();
var cred = await Esri.ArcGISRuntime.Security.AuthenticationManager.Current.GenerateCredentialAsync(
new Uri("https://www.arcgis.com/sharing/rest/generatetoken"),
user, pass, options);
//Check the credential and add to the identity manager
if (cred != null)
Esri.ArcGISRuntime.Security.AuthenticationManager.Current.AddCredential(cred);
//Create the service for the export
var tileService = new ArcGISTiledLayer(new Uri (StorageManager.CurrentBasemapUrl));
if (tileService.LoadStatus != Esri.ArcGISRuntime.LoadStatus.Loaded)
await tileService.LoadAsync ();
//Create the export task
_exportTileTask = await ExportTileCacheTask.CreateAsync(new Uri (StorageManager.CurrentBasemapUrl));
//Create the export parameters
_tileParameters = await _exportTileTask.CreateDefaultExportTileCacheParametersAsync (mapExtent,
tileService.TileInfo.LevelsOfDetail.ToArray()[0].Scale,
tileService.TileInfo.LevelsOfDetail.ToArray()[maxLOD].Scale);
}
catch {
throw;
}
}
/// <summary>
/// Downloads the tile cache.
/// </summary>
/// <returns>The tile cache.</returns>
/// <param name="mapName">Map name.</param>
/// <param name="mapExtent">Map extent.</param>
/// <param name="maxLOD">Max LOD.</param>
public static async Task<TileCache> DownloadTileCacheAsync(string mapName, Envelope mapExtent, int maxLOD)
{
try {
//Setup the export parameters and task
await SetupExportTileCacheAsync(mapExtent, maxLOD);
//Get the export path and file
string exportPath = Path.Combine (ConfigurationManager.AppDataDirectory, mapName);
exportPath = PathHelpers.CleanPath (exportPath);
//Create the directory if it doesn't exist
if (!System.IO.Directory.Exists(exportPath)) System.IO.Directory.CreateDirectory(exportPath);
string exportFile = Path.Combine(exportPath, "BasemapLayer.tpk");
if (System.IO.File.Exists(exportFile)) System.IO.File.Delete(exportFile);
//Export the tiles
Job<TileCache> exportJob = _exportTileTask.ExportTileCache(_tileParameters, exportFile);
//Create the handler for status updates
exportJob.JobChanged += (object sender, EventArgs evt) => {
#if WINDOWS_UWP
Debug.WriteLine("[EXPORT STATUS MESSAGE] " + exportJob.Status.ToString());
#else
Console.WriteLine("[EXPORT STATUS MESSAGE] " + exportJob.Status.ToString());
#endif
};
//Get the result
TileCache cache = await exportJob.GetResultAsync();
//Check the result
if (cache == null) {
ToastHelpers.ShowError("Download Error", "Unable to download the selected basemap.\n\n" + exportJob.Error.Message, 7);
}
else {
ToastHelpers.ShowSuccess("Basemap Download", "Successfully downloaded the basemap for offline use.", 5);
}
return cache;
}
catch (Exception ex) {
ToastHelpers.ShowError ("Tile Cache Download Error",
"An error occurred while downloading the tile cache.\n\n" + ex.Message, 7);
return null;
}
}
Thanks for your feedback. We will try to include sample/guide doc.
Meanwhile, here's a quick sample of ExportTileCacheTask using the portal item you provided. Note: You will need to use the same username/password credential you use when you login to arcgis.com.
Providing challenge method for AuthenticationManager will allow you to supply the credential. If you have other secured services, you might want to do a check on CredentialRequestInfo.ServiceUri.
For simplicity of this sample, I just used a specific area of interest (one that I know will not exceed the tile count) but you can also use your current viewpoint geometry. This sample uses default parameters as defined by the service but you should also be able to provide a different min/max scale, etc. This sample also use temp directory with some random guid name for tpk, the full file path just needs to be accessible to the app and if you need to reference tpk later might be best to give it a meaningful name. Basically it will create a job whose result is a tile cache. The tile cache or full file path to tpk can be used to create an ArcGISTiledLayer.
AuthenticationManager.Current.ChallengeHandler = new ChallengeHandler(OnChallenge);
MyMapView.Map = new Map(Basemap.CreateTopographic());
}
private async Task<Credential> OnChallenge(CredentialRequestInfo info)
{
var credential = await AuthenticationManager.Current.GenerateCredentialAsync(
new Uri("http://www.arcgis.com/sharing/rest"),
"<your username>",
"<your password>");
return credential;
}
private async void ExportTile_Click(object sender, RoutedEventArgs e)
{
var portalItem = await PortalItem.CreateAsync(new Uri("http://www.arcgis.com/home/item.html?id=226d23f076da478bba4589e7eae95952"));
var task = await ExportTileCacheTask.CreateAsync(portalItem.ServiceUrl);
//var areaOfInterest = MyMapView.GetCurrentViewpoint(ViewpointType.BoundingGeometry)?.TargetGeometry;
var areaOfInterest = Geometry.FromJson("{\"xmin\":-11807293.832783949,\"ymin\":4358614.8414058322,\"xmax\":-11806682.336557668,\"ymax\":4358915.865882393,\"spatialReference\":{\"wkid\":102100,\"latestWkid\":3857}}");
var minScale = task.ServiceInfo.MinScale;
var maxScale = task.ServiceInfo.MaxScale;
var parameters = await task.CreateDefaultExportTileCacheParametersAsync(areaOfInterest, minScale, maxScale);
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.tpk");
var job = task.ExportTileCache(parameters, path);
var tileCache = await job.GetResultAsync();
MyMapView.Map.OperationalLayers.Add(new ArcGISTiledLayer(tileCache));
MyMapView.SetViewpoint(new Viewpoint(areaOfInterest));
}