I used the samples to create an app that makes a local replica from a hosted feature service. I notice that every time the app starts, it creates a new replica that can be seen in the feature service's REST directory page. After several iterations of editing the code and starting the app to test it, the feature service has a long list of replicas. Other 'official' apps like Collector do not do this so there must be a way to have the app check for an existing and valid replica before creating a new one. What is the correct way to do this?
Solved! Go to Solution.
Hi Ken,
Yes this can be easily done. Basically, you need to check if the geodatabase exists. Since the app defines the geodatabase name. It needs to check for the same geodatabase. Infact you can also do just the sync if you would like so that app does a sync if are any changes between the local geodatabase copy and the online feature service. The workflow would be as soon as the application loads use serviceInfoTask.fetchFeatureServiceInfo(); And check if the geodatabse exists. If yes then you can directly add the layer else generate the geodatabase.
// Property to check if sync needs to be performed automatically
property bool syncEnabledInApp:false
Component.onCompleted: {
statusText.text = "Getting service info";
// If online
// Check for the local version if not available download one
// If available perform sync if auto sync is enabled
if(isAppOnline)
serviceInfoTask.fetchFeatureServiceInfo();
// If offline use the local version only
}
FileInfo{
id:fileInfo
filePath: gdbPath
}
ServiceInfoTask {
id: serviceInfoTask
url: featuresUrl
onFeatureServiceInfoStatusChanged: {
if (featureServiceInfoStatus === Enums.FeatureServiceInfoStatusCompleted) {
if(!serviceInfoTask.featureServiceInfo.isSyncEnabled){
statusText.text = qsTr("Error- Feature Service doesn't have sync capability."); return;
}
if(!serviceInfoTask.featureServiceInfo.layers[layerId].hasAttachments){
console.log("has no attachments")
statusText.text = qsTr("Error- Feature Layer doesn't support attachments.");
return;
}
//TODO: get the correct layer ID-unique one
//Get the featureservice name to append to the geodatabase.
statusText.text = qsTr("Feature Service info received for ",serviceInfoTask.featureServiceInfo.layers[layerId].name);
gdbPath = dataPath + serviceInfoTask.featureServiceInfo.layers[layerId].name + ".geodatabase"
console.log(gdbPath)
if(fileInfo.exists){
// perform sync -' Download only '
gdb.syncGeodatabaseParameters.syncDirection = Enums.SyncDirectionDownload
console.log("GDB valid ",gdb.valid)
if(gdb.valid){
if(syncEnabledInApp){
console.log("performing sync")
geodatabaseSyncTask.syncGeodatabase(gdb.syncGeodatabaseParameters, gdb);
busyIndicator.visible = true;
statusText.text = qsTr("Checking for any updates to download..");
}else{
// Add Local feature layer
offLineLayer.featureTable = local
mainMap.addLayer(offLineLayer)
}
}
}
else{
// Generate Geodatabase
generateGeodatabaseParameters.initialize(serviceInfoTask.featureServiceInfo);
generateGeodatabaseParameters.extent = mainMap.extent;
generateGeodatabaseParameters.returnAttachments = true;
busyIndicator.visible = true;
geodatabaseSyncTask.generateGeodatabase(generateGeodatabaseParameters, gdbPath,false);
statusText.text = qsTr("Downloading features with attachments for offline use..please wait");
}
} else if (featureServiceInfoStatus === Enums.FeatureServiceInfoStatusErrored) {
statusText.text = "Error:" + errorString;
//cancelButton.text = "Start Over";
}
}
}
Note:- It is up to app developer to provide a unique name for geodatabase in case everytime different feature service is being used. So that app knows which geodatabase corresponds to which feature service
I hope this helps,
Nakul
Hi Ken,
Yes this can be easily done. Basically, you need to check if the geodatabase exists. Since the app defines the geodatabase name. It needs to check for the same geodatabase. Infact you can also do just the sync if you would like so that app does a sync if are any changes between the local geodatabase copy and the online feature service. The workflow would be as soon as the application loads use serviceInfoTask.fetchFeatureServiceInfo(); And check if the geodatabse exists. If yes then you can directly add the layer else generate the geodatabase.
// Property to check if sync needs to be performed automatically
property bool syncEnabledInApp:false
Component.onCompleted: {
statusText.text = "Getting service info";
// If online
// Check for the local version if not available download one
// If available perform sync if auto sync is enabled
if(isAppOnline)
serviceInfoTask.fetchFeatureServiceInfo();
// If offline use the local version only
}
FileInfo{
id:fileInfo
filePath: gdbPath
}
ServiceInfoTask {
id: serviceInfoTask
url: featuresUrl
onFeatureServiceInfoStatusChanged: {
if (featureServiceInfoStatus === Enums.FeatureServiceInfoStatusCompleted) {
if(!serviceInfoTask.featureServiceInfo.isSyncEnabled){
statusText.text = qsTr("Error- Feature Service doesn't have sync capability."); return;
}
if(!serviceInfoTask.featureServiceInfo.layers[layerId].hasAttachments){
console.log("has no attachments")
statusText.text = qsTr("Error- Feature Layer doesn't support attachments.");
return;
}
//TODO: get the correct layer ID-unique one
//Get the featureservice name to append to the geodatabase.
statusText.text = qsTr("Feature Service info received for ",serviceInfoTask.featureServiceInfo.layers[layerId].name);
gdbPath = dataPath + serviceInfoTask.featureServiceInfo.layers[layerId].name + ".geodatabase"
console.log(gdbPath)
if(fileInfo.exists){
// perform sync -' Download only '
gdb.syncGeodatabaseParameters.syncDirection = Enums.SyncDirectionDownload
console.log("GDB valid ",gdb.valid)
if(gdb.valid){
if(syncEnabledInApp){
console.log("performing sync")
geodatabaseSyncTask.syncGeodatabase(gdb.syncGeodatabaseParameters, gdb);
busyIndicator.visible = true;
statusText.text = qsTr("Checking for any updates to download..");
}else{
// Add Local feature layer
offLineLayer.featureTable = local
mainMap.addLayer(offLineLayer)
}
}
}
else{
// Generate Geodatabase
generateGeodatabaseParameters.initialize(serviceInfoTask.featureServiceInfo);
generateGeodatabaseParameters.extent = mainMap.extent;
generateGeodatabaseParameters.returnAttachments = true;
busyIndicator.visible = true;
geodatabaseSyncTask.generateGeodatabase(generateGeodatabaseParameters, gdbPath,false);
statusText.text = qsTr("Downloading features with attachments for offline use..please wait");
}
} else if (featureServiceInfoStatus === Enums.FeatureServiceInfoStatusErrored) {
statusText.text = "Error:" + errorString;
//cancelButton.text = "Start Over";
}
}
}
Note:- It is up to app developer to provide a unique name for geodatabase in case everytime different feature service is being used. So that app knows which geodatabase corresponds to which feature service
I hope this helps,
Nakul
Thanks Nakul
This makes perfect sense. Now when I implemented it I was able to generate the replica and find it on subsequent executions so it only generates one replica on the service. But on the second and all subsequent executions the line if(gdb.valid) consistently returns false. So my next question is , what causes that and how do I correct it?
Thanks,
Ken
If your geodatabase is setup correctly it should return valid as true. Please check our Local geodatabase editing sample. That has the code for checking the gdb validity.
-Nakul
?Thanks for the suggestion, Nakul. I am pretty confident my service and geodatabase are configured correctly. Or at least I can download it in an offline map using Collector, edit the layer and synchronize the changes back to AGOL. Furthermore, if I make sure there are no existing local replicas on the device, I can download the layer to my app, which successfully creates a new local version of the data. I can then add features to the layer and sync it back to the service. It only fails on the gdb.valid method when I relaunch the app and it finds the existing local replica. So perhaps there are steps I should be taking when I close down the app in order to close the gdb, or the editing session. Perhaps there is something I am not doing and this corrupts the gdb for future use causing gdb.valid to return false?
Just trying to analyze this and figure it out.
Thanks for any help.
Ken
Hmm. Couple of things to try
1) Can you confirm this workflow- generate geodatabase locally for the first time. Then close the app (no edits). Next time open the app and load the local geodatabase (instead of regenerating). And check if the gdd is valid this time?
2) Other thing u might wanna try is to unregister ur geodatabase after edits and sync is performed. Next time before you open the same local geodatabase. Make sure to register it again before reading the geodatabase using registerGeodatabase()
ArcGIS Runtime SDK for Qt QML API: GeodatabaseSyncTask Class Reference
Can you confirm this workflow- generate geodatabase locally for the first time. Then close the app (no edits). Next tim?e open the app and load the local geodatabase (instead of regenerating). And check if the gdd is valid this time?
Yes, I generated the gdb locally, made no edits and closed the app. I then opened the app and watched the debug messages as it found the local gdb and gdb.valid returned false.
I can see about unregister/register. Is that a standard procedure for an offline editing app?
Thats weird. As per your last message I thought issue could be related to editing. But now it seems some other issue since you are having the issue even when you don't make any edits. I have some sample code below. I am not doing any editing here. But it generates the geodatabase the first time. And next time it opens the same geodatabase without generating again. May be you wanna take a look at that and see if this helps.
App {
id: app
width: 400
height: 640
property double scaleFactor: AppFramework.displayScaleFactor
property int fontSize: 15 * scaleFactor
property string featuresUrl: "http://services.arcgis.com/6DIQcwlPy8knb6sg/arcgis/rest/services/Campus_Tour_20140624_002207/FeatureServer"
property string dataPath: "~/ArcGIS/Runtime/Data/Test/t/"
property string gdbPath: ""
property var selectedFeatureId: null
property bool isAppOnline: AppFramework.network.isOnline
property int layerId:0
property bool syncEnabledInApp:false
Map {
id: mainMap
anchors {
left: parent.left
top: parent.top
right: parent.right
bottom: msgRow.top
}
focus: true
ArcGISTiledMapServiceLayer {
url: "http://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer"
}
onMouseClicked: {
if (!app.isOnline) {
offLineLayer.hitTestFeatures(mouse.x, mouse.y);
}
}
}
Column {
id: controlsColumn
anchors {
left: parent.left
top: parent.top
margins: 20 * scaleFactor
}
spacing: 7
}
Rectangle {
width: 200 * scaleFactor
height: 200 * scaleFactor
visible: attachmentsList.currentIndex != -1 ? true : false
color: "lightgrey"
radius: 5
border.color: "black"
opacity: 0.77
clip: true
ListView {
id: attachmentsList
property int itemHeight: 20
anchors {
fill: parent
margins: 5 * scaleFactor
}
visible: currentIndex != -1 ? true : false
header: Item {
height: attachmentsList.itemHeight * scaleFactor
width: parent.width
clip: true
Text {
text: "Attachments"; font { bold: true }
}
}
model: attachmentsModel
delegate: Item {
height: attachmentsList.itemHeight * scaleFactor
width: parent.width
clip: true
Text {
text: name
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
onClicked: {
attachmentsList.currentIndex = index
var item = attachmentsList.model.get(attachmentsList.currentIndex);
local.viewAttachment(item["attachmentId"]);
}
}
}
highlightFollowsCurrentItem: true
highlight: Rectangle {
height: attachmentsList.currentItem.height
color: "lightsteelblue"
}
focus: true
}
}
ListModel {
id: attachmentsModel
}
Rectangle {
anchors {
fill: attachmentImage
margins: -10 * scaleFactor
}
visible: attachmentImage.visible
color: "black"
radius: 5
border.color: "black"
opacity: 0.77
}
Image {
id: attachmentImage
anchors {
fill: parent
margins: 20 * scaleFactor
}
visible: attachmentImage.source != "" ? true : false
fillMode: Image.PreserveAspectFit
MouseArea {
anchors.fill: parent
onClicked: {
attachmentImage.source = ""
}
}
}
FeatureLayer {
id: offLineLayer
selectionColor: "cyan"
function hitTestFeatures(x,y) {
offLineLayer.clearSelection()
attachmentsModel.clear()
var featureIds = offLineLayer.findFeatures(x, y, 1, 1);
if (featureIds.length > 0) {
selectedFeatureId = featureIds[0];
selectFeature(selectedFeatureId);
statusText.text = "Selected featureID is "+ selectedFeatureId;
//if(local.hasAttachments)
local.queryAttachmentInfos(selectedFeatureId);
// else
//statusText.text = qsTr("Feature Layer has no attachments")
}
}
onStatusChanged: {
if(status === Enums.LayerStatusInitialized) {
console.log("Layer initialized")
//console.log(JSON.stringify(serviceInfoTask.featureServiceInfo.fullExtent.json))
//console.log(serviceInfoTask.featureServiceInfo.initialExtent.toText())
//console.log(JSON.stringify(offLineLayer.extent.json))
if(isAppOnline)
mainMap.zoomTo(serviceInfoTask.featureServiceInfo.fullExtent)
else
mainMap.zoomTo(offLineLayer.extent)
}
}
}
FileInfo{
id:fileInfo
filePath: gdbPath
}
FileFolder {
id: tempFolder
}
GeodatabaseFeatureTable {
id: local
geodatabase: gdb.valid ? gdb : null
featureServiceLayerId: layerId
onQueryAttachmentInfosStatusChanged: {
if(queryAttachmentInfosStatus === Enums.QueryAttachmentInfosStatusCompleted){
console.log("got attachments")
var count = 0;
for (var attachmentInfo in local.attachmentInfos) {
var info = local.attachmentInfos[attachmentInfo];
attachmentsList.model.insert(count,
{
"attachmentId": info["attachmentId"],
"contentType": info["contentType"],
"name": info["name"],
"size": info["size"]
})
}
if (attachmentsList.count > 0)
attachmentsList.currentIndex = 0;
count++;
}
}
onRetrieveAttachmentStatusChanged: {
if (local.retrieveAttachmentStatus === Enums.RetrieveAttachmentStatusCompleted) {
if (local.retrieveAttachmentResult !== null) {
if (local.retrieveAttachmentResult !== null) {
if (Qt.platform.os === "windows") {
var tempPath = tempFolder.path.split(":")[1];
var str = local.retrieveAttachmentResult.saveToFile("file://" + tempPath, true);
attachmentImage.source = "file://" + str.split(":")[1];
} else {
var str2 = local.retrieveAttachmentResult.saveToFile("file://" + tempFolder.path, true);
attachmentImage.source = "file://" + str2;
}
}
}
} else if (local.retrieveAttachmentStatus === Enums.RetrieveAttachmentStatusErrored) {
statusText.text = "Retrieve Attachment error: " + featureLayer.featureTable.retrieveAttachmentError;
}
}
function viewAttachment(attachmentId) {
local.retrieveAttachment(selectedFeatureId, attachmentId);
}
}
Geodatabase {
id: gdb
path: gdbPath
onValidChanged: {
if (valid) {
var gdbtables = gdb.geodatabaseFeatureTables;
for(var i in gdbtables) {
console.log (gdbtables.featureServiceLayerName);
}
}
}
}
ServiceInfoTask {
id: serviceInfoTask
url: featuresUrl
onFeatureServiceInfoStatusChanged: {
if (featureServiceInfoStatus === Enums.FeatureServiceInfoStatusCompleted) {
if(!serviceInfoTask.featureServiceInfo.isSyncEnabled){
statusText.text = qsTr("Error- Feature Service doesn't have sync capability.");
return;
}
if(!serviceInfoTask.featureServiceInfo.layers[layerId].hasAttachments){
console.log("has no attachments")
statusText.text = qsTr("Error- Feature Layer doesn't support attachments.");
return;
}
//TODO: get the correct layer ID-unique one
//Get the featureservice name to append to the geodatabase.
statusText.text = qsTr("Feature Service info received for ",serviceInfoTask.featureServiceInfo.layers[layerId].name);
gdbPath = dataPath + serviceInfoTask.featureServiceInfo.layers[layerId].name + ".geodatabase"
console.log(gdbPath)
if(fileInfo.exists){
// perform sync -' Download only '
gdb.syncGeodatabaseParameters.syncDirection = Enums.SyncDirectionDownload
console.log("GDB valid ",gdb.valid)
if(gdb.valid){
if(syncEnabledInApp){
console.log("performing sync")
geodatabaseSyncTask.syncGeodatabase(gdb.syncGeodatabaseParameters, gdb);
busyIndicator.visible = true;
statusText.text = qsTr("Checking for any updates to download..");
}else{
// Add Local feature layer
offLineLayer.featureTable = local
mainMap.addLayer(offLineLayer)
}
}
}
else{
// Generate Geodatabase
generateGeodatabaseParameters.initialize(serviceInfoTask.featureServiceInfo);
generateGeodatabaseParameters.extent = mainMap.extent;
generateGeodatabaseParameters.returnAttachments = true;
busyIndicator.visible = true;
geodatabaseSyncTask.generateGeodatabase(generateGeodatabaseParameters, gdbPath,false);
statusText.text = qsTr("Downloading features with attachments for offline use..please wait");
}
} else if (featureServiceInfoStatus === Enums.FeatureServiceInfoStatusErrored) {
statusText.text = "Error:" + errorString;
//cancelButton.text = "Start Over";
}
}
}
GeodatabaseSyncStatusInfo {
id: syncStatusInfo
}
GeodatabaseSyncTask {
id: geodatabaseSyncTask
url: featuresUrl
onGenerateStatusChanged: {
statusText.text = generateStatus;
if (generateStatus === Enums.GenerateStatusCompleted) {
statusText.text = geodatabasePath;
//cancelButton.enabled = false;
busyIndicator.visible = false;
//statusText.text = "Select a feature";
//console.log("generation completed")
// Add Local feature layer
gdb.path = geodatabasePath;
offLineLayer.featureTable = local
mainMap.addLayer(offLineLayer)
} else if (generateStatus === GeodatabaseSyncTask.GenerateError) {
statusText.text = "Error: " + generateGeodatabaseError.message + " Code= " + generateGeodatabaseError.code.toString() + " " + generateGeodatabaseError.details;
//cancelButton.text = "Start Over";
}
}
onGeodatabaseSyncStatusInfoChanged: {
}
onSyncStatusChanged: {
if (syncStatus === Enums.SyncStatusCompleted) {
//cancelButton.enabled = false;
statusText.text = qsTr("Sync completed. Select features to view attachments.");
busyIndicator.visible = false;
// Add Local feature layer
offLineLayer.featureTable = local
mainMap.addLayer(offLineLayer)
}
if (syncStatus === Enums.SyncStatusErrored){
statusText.text = "Error: " + syncGeodatabaseError.message + " Code= " + syncGeodatabaseError.code.toString() + " " + syncGeodatabaseError.details +". Adding layer from the local storage";
busyIndicator.visible = false;
// if fails still add local feature layer from last synced results
offLineLayer.featureTable = local
mainMap.addLayer(offLineLayer)
}
if (syncStatus === Enums.SyncStatusInProgress) {
statusText.text = "Downloading the updates...please wait"
}
}
}
GenerateGeodatabaseParameters {
id: generateGeodatabaseParameters
}
Rectangle {
anchors {
fill: msgRow
leftMargin: -10 * scaleFactor
}
color: "lightgrey"
border.color: "black"
opacity: 0.77
}
Row {
id: msgRow
anchors {
bottom: parent.bottom
left: parent.left
leftMargin: 10 * scaleFactor
right: parent.right
}
spacing: 10 * scaleFactor
BusyIndicator {
id: busyIndicator
anchors.verticalCenter: parent.verticalCenter
enabled: false
visible: enabled
height: (parent.height * 0.5) * scaleFactor
width: height * scaleFactor
}
Text {
id: statusText
anchors.bottom: parent.bottom
width: parent.width
wrapMode: Text.WordWrap
font.pixelSize: fontSize
}
}
Rectangle {
anchors.fill: parent
color: "transparent"
border {
width: 0.5 * scaleFactor
color: "black"
}
}
Component.onCompleted: {
statusText.text = "Getting service info";
// If online
// Check for the local version if not available download one
// If available perform sync
if(isAppOnline)
serviceInfoTask.fetchFeatureServiceInfo();
// If offline use the local version only
// ToDO -ask user to choose the local geodatabase
}
}
Hey Nakul
Thanks for the code. Comparing it to my code, I found an error in my code that caused it to lose the path to the local GDB after creation which is what caused GDB.valid to return false. That part is working now. Now on to fix other problems. Thanks again.
Ken
I am glad I was able to help you to resolve this issue.
Cheers,
Nakul