Synchronize offline edits with a feature service.

Use case
A survey worker who works in an area without an internet connection could take a geodatabase of survey features offline at their office, make edits and add new features to the offline geodatabase in the field, and sync the updates with the online feature service after returning to the office.
How to use the sample
Pan and zoom to position the red rectangle around the area you want to take offline. Click “Generate geodatabase” to take the area offline. When complete, the map will update to only show the offline area. To edit features, click to select a feature, and click again anywhere else on the map to move the selected feature to the clicked location. To sync the edits with the feature service, click the “Sync geodatabase” button.
How it works
- Create a
GeodatabaseSyncTaskfrom a URL to a feature service. - Create
GenerateGeodatabaseParameters, passing in anEnvelopeextent as the parameter. - Create a
GenerateGeodatabaseJobfrom theGeodatabaseSyncTaskusinggenerateGeodatabase(...), passing in the parameters and a path to where the geodatabase should be downloaded locally. - Start the job and get the result
Geodatabase. - Load the geodatabase and get its feature tables. Create feature layers from the feature tables and add them to the map’s operational layers collection.
- Create
SyncGeodatabaseParametersand set the sync direction. - Create a
SyncGeodatabaseJobfromGeodatabaseSyncTaskusingsyncGeodatabase(...)passing in the parameters and geodatabase as arguments. - Start the sync job to synchronize the edits.
Relevant API
- FeatureLayer
- FeatureTable
- GenerateGeodatabaseJob
- GenerateGeodatabaseParameters
- GeodatabaseSyncTask
- SyncGeodatabaseJob
- SyncGeodatabaseParameters
- SyncLayerOption
Offline Data
To set up the sample’s offline data, see the Use offline data in the samples section of the Qt Samples repository overview.
| Link | Local Location |
|---|---|
| San Francisco Streets TPKX | <userhome>/ArcGIS/Runtime/Data/tpkx/SanFrancisco.tpkx |
About the data
The basemap uses an offline tile package of San Francisco. The online feature service has features with wildfire information.
Tags
feature service, geodatabase, offline, synchronize
Sample Code
// [WriteFile Name=EditAndSyncFeatures, Category=EditData]// [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 "EditAndSyncFeatures.h"
// ArcGIS Maps SDK headers#include "ArcGISFeature.h"#include "ArcGISTiledLayer.h"#include "Basemap.h"#include "Envelope.h"#include "Error.h"#include "FeatureLayer.h"#include "GenerateGeodatabaseJob.h"#include "GenerateGeodatabaseParameters.h"#include "GenerateLayerOption.h"#include "Geodatabase.h"#include "GeodatabaseFeatureTable.h"#include "GeodatabaseSyncTask.h"#include "GeometryEngine.h"#include "IdentifyLayerResult.h"#include "LayerListModel.h"#include "Map.h"#include "MapQuickView.h"#include "MapViewTypes.h"#include "Point.h"#include "ServiceFeatureTable.h"#include "SpatialReference.h"#include "SyncGeodatabaseJob.h"#include "SyncLayerOption.h"#include "TaskTypes.h"#include "TileCache.h"#include "Viewpoint.h"
// Qt headers#include <QFuture>#include <QStandardPaths>#include <QUrl>#include <QUuid>#include <QtCore/qglobal.h>
using namespace Esri::ArcGISRuntime;
// helper method to get cross platform data pathnamespace{ QString defaultDataPath() { QString dataPath;
#ifdef Q_OS_IOS dataPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); #else dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); #endif
return dataPath; }} // namespace
EditAndSyncFeatures::EditAndSyncFeatures(QQuickItem* parent /* = nullptr */): QQuickItem(parent), m_dataPath(defaultDataPath() + "/ArcGIS/Runtime/Data/"){}
EditAndSyncFeatures::~EditAndSyncFeatures(){ // unregister geodatabase auto f = m_syncTask->unregisterGeodatabaseAsync(m_offlineGdb); Q_UNUSED(f)}
void EditAndSyncFeatures::init(){ qmlRegisterType<MapQuickView>("Esri.Samples", 1, 0, "MapView"); qmlRegisterType<EditAndSyncFeatures>("Esri.Samples", 1, 0, "EditAndSyncFeaturesSample");}
void EditAndSyncFeatures::componentComplete(){ QQuickItem::componentComplete();
// find QML MapView component m_mapView = findChild<MapQuickView*>("mapView"); m_mapView->setWrapAroundMode(WrapAroundMode::Disabled);
// Create a map using a local tile package TileCache* tileCache = new TileCache(m_dataPath + "tpkx/SanFrancisco.tpkx", this); ArcGISTiledLayer* tiledLayer = new ArcGISTiledLayer(tileCache, this); Basemap* basemap = new Basemap(tiledLayer, this); m_map = new Map(basemap, this);
// add a feature service to the map ServiceFeatureTable* featureTable = new ServiceFeatureTable(QUrl(m_featureServiceUrl + QString::number(m_featureLayerId)), this); FeatureLayer* featureLayer = new FeatureLayer(featureTable, this); m_map->operationalLayers()->append(featureLayer);
// set an initial viewpoint Envelope env(-122.50017, 37.74500, -122.43843, 37.81638, SpatialReference(4326)); Viewpoint viewpoint(env); m_map->setInitialViewpoint(viewpoint);
// Set map to map view m_mapView->setMap(m_map);
// create the GeodatabaseSyncTask m_syncTask = new GeodatabaseSyncTask(QUrl(m_featureServiceUrl), this);
connectSignals();}
void EditAndSyncFeatures::connectSignals(){ // lambda expression for the mouse clicked signal on the mapview connect(m_mapView, &MapQuickView::mouseClicked, this, [this](QMouseEvent& mouseEvent) { if (m_isOffline) { if (!m_selectedFeature) { m_mapView->identifyLayerAsync(m_map->operationalLayers()->first(), mouseEvent.position(), 5, false, 1).then(this, [this](IdentifyLayerResult* identifyResult) { // once the identify is done if (!identifyResult) return;
// clear any existing selection FeatureLayer* featureLayer = static_cast<FeatureLayer*>(m_map->operationalLayers()->first()); featureLayer->clearSelection(); m_selectedFeature = nullptr;
if (identifyResult->geoElements().length() > 0) { GeoElement* geoElement = identifyResult->geoElements().at(0); m_selectedFeature = static_cast<ArcGISFeature*>(geoElement); featureLayer->selectFeature(m_selectedFeature); emit updateInstruction("Tap on map to move feature"); } }); } else { // connect to feature table signal FeatureLayer* featureLayer = static_cast<FeatureLayer*>(m_map->operationalLayers()->at(0));
// get the point from the mouse point const Point mapPoint = m_mapView->screenToLocation(mouseEvent.position().x(), mouseEvent.position().y()); m_selectedFeature->setGeometry(mapPoint); featureLayer->featureTable()->updateFeatureAsync(m_selectedFeature).then(this, [this, featureLayer]() { featureLayer->clearSelection(); m_selectedFeature = nullptr; emit updateInstruction("Tap the sync button"); emit showButton(); }); } } });}
//! [EditAndSyncFeatures parameters generate]GenerateGeodatabaseParameters EditAndSyncFeatures::getGenerateParameters(Envelope gdbExtent){ // create the parameters GenerateGeodatabaseParameters params; params.setReturnAttachments(false); params.setOutSpatialReference(SpatialReference::webMercator()); params.setExtent(gdbExtent);
// set the layer options for 1 layer QList<GenerateLayerOption> layerOptions; GenerateLayerOption generateLayerOption(m_featureLayerId); layerOptions << generateLayerOption; params.setLayerOptions(layerOptions);
return params;}//! [EditAndSyncFeatures parameters generate]
//! [EditAndSyncFeatures parameters sync]SyncGeodatabaseParameters EditAndSyncFeatures::getSyncParameters(){ // create the parameters SyncGeodatabaseParameters params; params.setGeodatabaseSyncDirection(SyncDirection::Bidirectional);
// set the layer options for 1 layer QList<SyncLayerOption> layerOptions; SyncLayerOption syncLayerOption(m_featureLayerId); layerOptions << syncLayerOption; params.setLayerOptions(layerOptions);
return params;}//! [EditAndSyncFeatures parameters sync]
//! [EditAndSyncFeatures generate]void EditAndSyncFeatures::generateGeodatabaseFromCorners(double xCorner1, double yCorner1, double xCorner2, double yCorner2){ // create an envelope from the QML rectangle corners const Point corner1 = m_mapView->screenToLocation(xCorner1, yCorner1); const Point corner2 = m_mapView->screenToLocation(xCorner2, yCorner2); const Envelope extent(corner1, corner2); const Envelope geodatabaseExtent = geometry_cast<Envelope>(GeometryEngine::project(extent, SpatialReference::webMercator()));
// get the updated parameters GenerateGeodatabaseParameters params = getGenerateParameters(geodatabaseExtent);
// execute the task and obtain the job const QString outputGdb = m_temporaryDir.path() + "/Wildfire.geodatabase"; GenerateGeodatabaseJob* generateJob = m_syncTask->generateGeodatabase(params, outputGdb);
// connect to the job's status changed signal if (generateJob) { connect(generateJob, &GenerateGeodatabaseJob::statusChanged, this, [this, generateJob](JobStatus jobStatus) { // connect to the job's status changed signal to know once it is done switch (jobStatus) { case JobStatus::Failed: emit updateStatus("Generate failed"); emit hideWindow(5000, false); break; case JobStatus::NotStarted: emit updateStatus("Job not started"); break; case JobStatus::Paused: emit updateStatus("Job paused"); break; case JobStatus::Started: emit updateStatus("In progress..."); break; case JobStatus::Succeeded: m_isOffline = true; m_offlineGdb = generateJob->result(); emit isOfflineChanged(); emit updateStatus("Complete"); emit hideWindow(1500, true); emit updateInstruction("Tap on a feature"); addOfflineData(); break; default: break; } });
// start the generate job generateJob->start(); } //! [EditAndSyncFeatures generate] else { emit updateStatus("Generate failed"); emit hideWindow(5000, false); }}
void EditAndSyncFeatures::addOfflineData(){ // remove the original online feature layers m_map->operationalLayers()->clear();
// load the geodatabase connect(m_offlineGdb, &Geodatabase::doneLoading, this, [this](Error) { // create a feature layer from each feature table, and add to the map const auto tables = m_offlineGdb->geodatabaseFeatureTables(); for (GeodatabaseFeatureTable* featureTable : tables) { FeatureLayer* featureLayer = new FeatureLayer(featureTable, this); m_map->operationalLayers()->append(featureLayer); } }); m_offlineGdb->load();}
bool EditAndSyncFeatures::isOffline() const{ return m_isOffline;}
//! [EditAndSyncFeatures executeSync]void EditAndSyncFeatures::executeSync(){ // get the updated parameters SyncGeodatabaseParameters params = getSyncParameters();
// execute the task and obtain the job SyncGeodatabaseJob* syncJob = m_syncTask->syncGeodatabase(params, m_offlineGdb);
// connect to the job's status changed signal if (syncJob) { connect(syncJob, &GenerateGeodatabaseJob::statusChanged, this, [this](JobStatus jobStatus) { // connect to the job's status changed signal to know once it is done switch (jobStatus) { case JobStatus::Failed: emit updateStatus("Sync failed"); emit hideWindow(5000, false); break; case JobStatus::NotStarted: emit updateStatus("Job not started"); break; case JobStatus::Paused: emit updateStatus("Job paused"); break; case JobStatus::Started: emit updateStatus("In progress..."); break; case JobStatus::Succeeded: m_isOffline = true; emit isOfflineChanged(); emit updateStatus("Complete"); emit hideWindow(1500, true); emit updateInstruction("Tap on a feature"); break; default: break; } });
// start the sync job syncJob->start(); } //! [EditAndSyncFeatures executeSync] else { emit updateStatus("Sync failed"); emit hideWindow(5000, false); }}// [WriteFile Name=EditAndSyncFeatures, Category=EditData]// [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]
#ifndef EDITANDSYNCFEATURES_H#define EDITANDSYNCFEATURES_H
// ArcGIS Maps SDK headers#include "GenerateGeodatabaseParameters.h"#include "SyncGeodatabaseParameters.h"
// Qt headers#include <QQuickItem>#include <QString>#include <QTemporaryDir>#include <QUrl>
namespace Esri::ArcGISRuntime{ class Map; class MapQuickView; class GeodatabaseSyncTask; class Geodatabase; class ArcGISFeature; class Envelope;}
class EditAndSyncFeatures : public QQuickItem{ Q_OBJECT
Q_PROPERTY(bool isOffline READ isOffline NOTIFY isOfflineChanged)
public: explicit EditAndSyncFeatures(QQuickItem* parent = nullptr); ~EditAndSyncFeatures() override;
void componentComplete() override; static void init(); Q_INVOKABLE void generateGeodatabaseFromCorners(double xCorner1, double yCorner1, double xCorner2, double yCorner2); Q_INVOKABLE void executeSync();
signals: void updateStatus(QString status); void hideWindow(int time, bool success); void updateInstruction(QString instruction); void isOfflineChanged(); void showButton();
private: void connectSignals(); Esri::ArcGISRuntime::GenerateGeodatabaseParameters getGenerateParameters(Esri::ArcGISRuntime::Envelope gdbExtent); Esri::ArcGISRuntime::SyncGeodatabaseParameters getSyncParameters(); void addOfflineData(); bool isOffline() const;
private: Esri::ArcGISRuntime::Map* m_map = nullptr; Esri::ArcGISRuntime::MapQuickView* m_mapView = nullptr; Esri::ArcGISRuntime::GeodatabaseSyncTask* m_syncTask = nullptr; Esri::ArcGISRuntime::Geodatabase* m_offlineGdb = nullptr; Esri::ArcGISRuntime::ArcGISFeature* m_selectedFeature = nullptr; QString m_dataPath; qint64 m_featureLayerId = 0; QString m_featureServiceUrl = QStringLiteral("https://sampleserver6.arcgisonline.com/arcgis/rest/services/Sync/WildfireSync/FeatureServer/"); bool m_isOffline = false; QTemporaryDir m_temporaryDir;};
#endif // EDITANDSYNCFEATURES_H// [WriteFile Name=EditAndSyncFeatures, Category=EditData]// [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]
import QtQuickimport QtQuick.Controlsimport Esri.Samples
EditAndSyncFeaturesSample { id: editAndSyncSample clip: true width: 800 height: 600
property string statusText: "" property string instructionText: "" property var selectedFeature: null
// add a mapView component MapView { anchors.fill: parent objectName: "mapView"
Component.onCompleted: { // Set the focus on MapView to initially enable keyboard navigation forceActiveFocus(); } }
onHideWindow: (time, success) => { syncWindow.hideWindow(time);
if (success) { extentRectangle.visible = false; syncButton.visible = false; } }
onUpdateInstruction: instruction => { instructions.visible = true; instructionText = instruction; }
onShowButton: syncButton.visible = true;
onUpdateStatus: status => statusText = status;
// create an extent rectangle for the output geodatabase Rectangle { id: extentRectangle anchors.centerIn: parent width: parent.width - (50) height: parent.height - (125) color: "transparent" border { color: "red" width: 3 } }
// Create the button to generate/sync geodatabase Rectangle { id: syncButton property bool pressed: false anchors { horizontalCenter: parent.horizontalCenter bottom: parent.bottom bottomMargin: 23 }
width: isOffline ? 175 : 200 height: 35 color: pressed ? "#959595" : "#D6D6D6" radius: 8 border { color: "#585858" width: 1 }
Row { anchors.fill: parent spacing: 5 Image { width: 38 height: width source: isOffline ? "qrc:/Samples/EditData/EditAndSyncFeatures/sync.png" : "qrc:/Samples/EditData/EditAndSyncFeatures/download.png" }
Text { anchors.verticalCenter: parent.verticalCenter text: isOffline ? "Sync Geodatabase" : "Generate Geodatabase" font.pixelSize: 14 color: "#474747" } }
MouseArea { anchors.fill: parent onPressed: syncButton.pressed = true onReleased: syncButton.pressed = false onClicked: { if (isOffline) { instructions.visible = false; editAndSyncSample.executeSync(); syncWindow.visible = true; } else { editAndSyncSample.generateGeodatabaseFromCorners(extentRectangle.x, extentRectangle.y, (extentRectangle.x + extentRectangle.width), (extentRectangle.y + extentRectangle.height)); syncWindow.visible = true; } } } }
// Create a bar to display editing instructions Rectangle { id: instructions anchors { top: parent.top left: parent.left right: parent.right } height: 25 color: "gray" opacity: 0.9 visible: false
Text { anchors.centerIn: parent text: instructionText font.pixelSize: 16 color: "white" } }
// Create a window to display the generate/sync window Rectangle { id: syncWindow anchors.fill: parent color: "transparent" clip: true visible: false
Rectangle { anchors.fill: parent color: "#60000000" }
MouseArea { anchors.fill: parent onClicked: mouse => mouse.accepted = true onWheel: wheel => wheel.accepted = true }
Rectangle { anchors.centerIn: parent width: 125 height: 100 color: "lightgrey" opacity: 0.8 radius: 5 border { color: "#4D4D4D" width: 1 }
Column { anchors { fill: parent margins: 10 } spacing: 10
BusyIndicator { anchors.horizontalCenter: parent.horizontalCenter }
Text { anchors.horizontalCenter: parent.horizontalCenter text: statusText font.pixelSize: 16 } } }
Timer { id: hideWindowTimer
onTriggered: { syncWindow.visible = false; instructions.visible = true; instructionText = "Tap on a feature"; } }
function hideWindow(time) { hideWindowTimer.interval = time; hideWindowTimer.restart(); } }}// [Legal]// Copyright 2015 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]
// sample headers#include "EditAndSyncFeatures.h"
// ArcGIS Maps SDK headers#include "ArcGISRuntimeEnvironment.h"
// Qt headers#include <QCommandLineParser>#include <QDir>#include <QGuiApplication>#include <QQmlEngine>#include <QQuickView>
// Platform specific headers#ifdef Q_OS_WIN#include <Windows.h>#endif
#define STRINGIZE(x) #x#define QUOTE(x) STRINGIZE(x)
int main(int argc, char *argv[]){ Esri::ArcGISRuntime::ArcGISRuntimeEnvironment::setUseLegacyAuthentication(false); QGuiApplication app(argc, argv); app.setApplicationName(QString("Edit and Sync Features"));
// Use of ArcGIS location services, such as basemap styles, geocoding, and routing services, // requires an access token. For more information see // https://links.esri.com/arcgis-runtime-security-auth.
// The following methods grant an access token:
// 1. User authentication: Grants a temporary access token associated with a user's ArcGIS account. // To generate a token, a user logs in to the app with an ArcGIS account that is part of an // organization in ArcGIS Online or ArcGIS Enterprise.
// 2. API key authentication: Get a long-lived access token that gives your application access to // ArcGIS location services. Go to the tutorial at https://links.esri.com/create-an-api-key. // Copy the API Key access token.
const QString accessToken = QString("");
if (accessToken.isEmpty()) { qWarning() << "Use of ArcGIS location services, such as the basemap styles service, requires" << "you to authenticate with an ArcGIS account or set the API Key property."; } else { Esri::ArcGISRuntime::ArcGISRuntimeEnvironment::setApiKey(accessToken); }
// Initialize the sample EditAndSyncFeatures::init();
// Initialize application view QQuickView view; view.setResizeMode(QQuickView::SizeRootObjectToView);
// Add the import Path view.engine()->addImportPath(QDir(QCoreApplication::applicationDirPath()).filePath("qml"));
QString arcGISRuntimeImportPath = QUOTE(ARCGIS_RUNTIME_IMPORT_PATH);
#if defined(LINUX_PLATFORM_REPLACEMENT) // on some linux platforms the string 'linux' is replaced with 1 // fix the replacement paths which were created QString replaceString = QUOTE(LINUX_PLATFORM_REPLACEMENT); arcGISRuntimeImportPath = arcGISRuntimeImportPath.replace(replaceString, "linux", Qt::CaseSensitive); #endif
// Add the Runtime and Extras path view.engine()->addImportPath(arcGISRuntimeImportPath);
// Set the source view.setSource(QUrl("qrc:/Samples/EditData/EditAndSyncFeatures/EditAndSyncFeatures.qml"));
view.show();
return app.exec();}