Generate a local geodatabase from an online feature service.
Use case
Generating geodatabases is the first step toward taking a feature service offline. It allows you to save features locally for offline display.
How to use the sample
Zoom to any extent. Then tap the generate button to generate a geodatabase of features from a feature service filtered to the current extent. A red outline will show the extent used. The job's progress is shown while the geodatabase is generated.
How it works
- Create a
GeodatabaseSyncTask
with the URL of the feature service and load it. - Create
GenerateGeodatabaseParameters
specifying the extent and whether to include attachments. - Create a
GenerateGeodatabaseJob
withgeodatabaseSyncTask.GenerateGeodatabaseAsync(parameters, downloadPath)
. Start the job withjob.Start()
. - When the job is done,
job.GetResultAsync()
will return the geodatabase. Inside the geodatabase are feature tables which can be used to add feature layers to the map. - Call
syncTask.UnregisterGeodatabaseAsync(geodatabase)
after generation when you're not planning on syncing changes to the service.
Relevant API
- GenerateGeodatabaseJob
- GenerateGeodatabaseParameters
- Geodatabase
- GeodatabaseSyncTask
Tags
disconnected, local geodatabase, offline, sync
Sample Code
// 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.
using Android.App;
using Android.OS;
using Android.Widget;
using ArcGISRuntime.Samples.Managers;
using Esri.ArcGISRuntime.Data;
using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.Mapping;
using Esri.ArcGISRuntime.Symbology;
using Esri.ArcGISRuntime.Tasks;
using Esri.ArcGISRuntime.Tasks.Offline;
using Esri.ArcGISRuntime.UI;
using Esri.ArcGISRuntime.UI.Controls;
using System;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Esri.ArcGISRuntime.ArcGISServices;
namespace ArcGISRuntime.Samples.GenerateGeodatabase
{
[Activity (ConfigurationChanges=Android.Content.PM.ConfigChanges.Orientation | Android.Content.PM.ConfigChanges.ScreenSize)]
[ArcGISRuntime.Samples.Shared.Attributes.OfflineData("e4a398afe9a945f3b0f4dca1e4faccb5")]
[ArcGISRuntime.Samples.Shared.Attributes.Sample(
name: "Generate geodatabase",
category: "Data",
description: "Generate a local geodatabase from an online feature service.",
instructions: "Zoom to any extent. Then tap the generate button to generate a geodatabase of features from a feature service filtered to the current extent. A red outline will show the extent used. The job's progress is shown while the geodatabase is generated.",
tags: new[] { "disconnected", "local geodatabase", "offline", "sync" })]
public class GenerateGeodatabase : Activity
{
// URL for a feature service that supports geodatabase generation.
private Uri _featureServiceUri = new Uri("https://sampleserver6.arcgisonline.com/arcgis/rest/services/Sync/WildfireSync/FeatureServer");
// Path to the geodatabase file on disk.
private string _gdbPath;
// Task to be used for generating the geodatabase.
private GeodatabaseSyncTask _gdbSyncTask;
// Job used to generate the geodatabase.
private GenerateGeodatabaseJob _generateGdbJob;
// Mapview.
private MapView myMapView;
// Generate Button.
private Button myGenerateButton;
// Progress indicator.
private AlertDialog alertDialog;
private ProgressBar progressIndicator;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
Title = "Generate geodatabase";
// Create the UI, setup the control references and execute initialization.
CreateLayout();
Initialize();
}
private void CreateLayout()
{
// Create the layout.
LinearLayout layout = new LinearLayout(this)
{
Orientation = Orientation.Vertical
};
// Add the generate button.
myGenerateButton = new Button(this)
{
Text = "Generate",
Enabled = false
};
myGenerateButton.Click += GenerateButton_Clicked;
layout.AddView(myGenerateButton);
// Add the mapview.
myMapView = new MapView(this);
layout.AddView(myMapView);
// Add the layout to the view.
SetContentView(layout);
// Create the progress dialog display.
progressIndicator = new ProgressBar(this);
AlertDialog.Builder builder = new AlertDialog.Builder(this).SetView(progressIndicator);
builder.SetMessage("Generating geodatabase");
alertDialog = builder.Create();
}
private async void Initialize()
{
// Create a tile cache and load it with the SanFrancisco streets tpk.
try
{
TileCache tileCache = new TileCache(DataManager.GetDataFolder("e4a398afe9a945f3b0f4dca1e4faccb5", "SanFrancisco.tpkx"));
// Create the corresponding layer based on the tile cache.
ArcGISTiledLayer tileLayer = new ArcGISTiledLayer(tileCache);
// Create the basemap based on the tile cache.
Basemap sfBasemap = new Basemap(tileLayer);
// Create the map with the tile-based basemap.
Map myMap = new Map(sfBasemap);
// Assign the map to the MapView.
myMapView.Map = myMap;
// Create a new symbol for the extent graphic.
SimpleLineSymbol lineSymbol = new SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.Red, 2);
// Create graphics overlay for the extent graphic and apply a renderer.
GraphicsOverlay extentOverlay = new GraphicsOverlay
{
Renderer = new SimpleRenderer(lineSymbol)
};
// Add graphics overlay to the map view.
myMapView.GraphicsOverlays.Add(extentOverlay);
// Set up an event handler for when the viewpoint (extent) changes.
myMapView.ViewpointChanged += MapViewExtentChanged;
// Create a task for generating a geodatabase (GeodatabaseSyncTask).
_gdbSyncTask = await GeodatabaseSyncTask.CreateAsync(_featureServiceUri);
// Add all graphics from the service to the map.
foreach (IdInfo layer in _gdbSyncTask.ServiceInfo.LayerInfos)
{
// Create the ServiceFeatureTable for this particular layer.
ServiceFeatureTable onlineTable = new ServiceFeatureTable(new Uri(_featureServiceUri + "/" + layer.Id));
// Wait for the table to load.
await onlineTable.LoadAsync();
// Add the layer to the map's operational layers if load succeeds.
if (onlineTable.LoadStatus == Esri.ArcGISRuntime.LoadStatus.Loaded)
{
myMap.OperationalLayers.Add(new FeatureLayer(onlineTable));
}
}
// Update the graphic - in case user doesn't interact with the map.
UpdateMapExtent();
// Enable the generate button now that the sample is ready.
myGenerateButton.Enabled = true;
}
catch (Exception ex)
{
ShowStatusMessage(ex.ToString());
}
}
private void UpdateMapExtent()
{
// Return if mapview is null.
if (myMapView == null) { return; }
// Get the new viewpoint.
Viewpoint myViewPoint = myMapView.GetCurrentViewpoint(ViewpointType.BoundingGeometry);
// Return if viewpoint is null.
if (myViewPoint == null) { return; }
// Get the updated extent for the new viewpoint.
Envelope extent = myViewPoint.TargetGeometry as Envelope;
// Return if extent is null.
if (extent == null) { return; }
// Create an envelope that is a bit smaller than the extent.
EnvelopeBuilder envelopeBldr = new EnvelopeBuilder(extent);
envelopeBldr.Expand(0.80);
// Get the (only) graphics overlay in the map view.
GraphicsOverlay extentOverlay = myMapView.GraphicsOverlays.FirstOrDefault();
// Return if the extent overlay is null.
if (extentOverlay == null) { return; }
// Get the extent graphic.
Graphic extentGraphic = extentOverlay.Graphics.FirstOrDefault();
// Create the extent graphic and add it to the overlay if it doesn't exist.
if (extentGraphic == null)
{
extentGraphic = new Graphic(envelopeBldr.ToGeometry());
extentOverlay.Graphics.Add(extentGraphic);
}
else
{
// Otherwise, update the graphic's geometry.
extentGraphic.Geometry = envelopeBldr.ToGeometry();
}
}
private async Task StartGeodatabaseGeneration()
{
// Update the geodatabase path.
_gdbPath = $"{Path.GetTempFileName()}.geodatabase";
// Create a task for generating a geodatabase (GeodatabaseSyncTask).
_gdbSyncTask = await GeodatabaseSyncTask.CreateAsync(_featureServiceUri);
// Get the current extent of the red preview box.
Envelope extent = myMapView.GraphicsOverlays.First().Graphics.First().Geometry as Envelope;
// Get the default parameters for the generate geodatabase task.
GenerateGeodatabaseParameters generateParams = await _gdbSyncTask.CreateDefaultGenerateGeodatabaseParametersAsync(extent);
// Create a generate geodatabase job.
_generateGdbJob = _gdbSyncTask.GenerateGeodatabase(generateParams, _gdbPath);
// Handle the progress changed event (to show progress bar).
_generateGdbJob.ProgressChanged += (sender, e) =>
{
UpdateProgressBar();
};
// Show the progress bar.
alertDialog.Show();
// Start the job.
_generateGdbJob.Start();
// Wait for the geodatabase.
Geodatabase resultGdb = await _generateGdbJob.GetResultAsync();
// Hide the progress bar.
alertDialog.Dismiss();
// Do the rest of the work.
await HandleGenerationCompleted(_generateGdbJob, resultGdb);
}
private async Task HandleGenerationCompleted(GenerateGeodatabaseJob job, Geodatabase resultGdb)
{
JobStatus status = job.Status;
// If the job completed successfully, add the geodatabase data to the map.
if (status == JobStatus.Succeeded)
{
// Clear out the existing layers.
myMapView.Map.OperationalLayers.Clear();
// Loop through all feature tables in the geodatabase and add a new layer to the map.
foreach (GeodatabaseFeatureTable table in resultGdb.GeodatabaseFeatureTables)
{
// Create a new feature layer for the table.
FeatureLayer _layer = new FeatureLayer(table);
// Add the new layer to the map.
myMapView.Map.OperationalLayers.Add(_layer);
}
// Best practice is to unregister the geodatabase.
await _gdbSyncTask.UnregisterGeodatabaseAsync(resultGdb);
// Tell the user that the geodatabase was unregistered.
ShowStatusMessage("Since no edits will be made, the local geodatabase has been unregistered per best practice.");
// Re-enable the generate button.
myGenerateButton.Enabled = true;
}
// See if the job failed.
if (status == JobStatus.Failed)
{
// Create a message to show the user.
string message = "Generate geodatabase job failed";
// Show an error message (if there is one).
if (job.Error != null)
{
message += ": " + job.Error.Message;
}
else
{
// If no error, show messages from the job.
message += ": " + string.Join("\n", job.Messages.Select(m => m.Message));
}
// Show error message.
ShowStatusMessage(message);
// Re-enable the generate button.
myGenerateButton.Enabled = true;
}
}
private void ShowStatusMessage(string message)
{
// Display the message to the user.
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.SetMessage(message).SetTitle("Alert").Show();
}
private async void GenerateButton_Clicked(object sender, EventArgs e)
{
// Fix the extent of the graphic.
myMapView.ViewpointChanged -= MapViewExtentChanged;
try
{
// Disable the generate button.
myGenerateButton.Enabled = false;
// Call the geodatabase generation method.
await StartGeodatabaseGeneration();
}
catch (Exception ex)
{
ShowStatusMessage(ex.ToString());
}
}
private void MapViewExtentChanged(object sender, EventArgs e)
{
// Call the map extent update method.
UpdateMapExtent();
}
private void UpdateProgressBar()
{
// Due to the nature of the threading implementation,
// the dispatcher needs to be used to interact with the UI.
RunOnUiThread(() =>
{
// Update the progress bar value.
progressIndicator.Progress = _generateGdbJob.Progress;
alertDialog.SetMessage($"Generating geodatabase ({_generateGdbJob.Progress}%)");
});
}
}
}