Offline editing: how to check for existing local replica before creating a new one

1251
9
Jump to solution
05-08-2017 07:27 AM
KenGorton
Esri Contributor

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?

0 Kudos
1 Solution

Accepted Solutions
nakulmanocha
Esri Regular Contributor

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

View solution in original post

0 Kudos
9 Replies
nakulmanocha
Esri Regular Contributor

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

0 Kudos
KenGorton
Esri Contributor

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

0 Kudos
nakulmanocha
Esri Regular Contributor

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

0 Kudos
KenGorton
Esri Contributor

?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

0 Kudos
nakulmanocha
Esri Regular Contributor

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 

0 Kudos
KenGorton
Esri Contributor

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?

0 Kudos
nakulmanocha
Esri Regular Contributor
  • 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.

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.

  • I can see about  unregister/register. Is that a standard procedure for an offline editing app?Yes. But you don't have to. I was just suggesting to see if this can fix the problem.

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



    }
}
0 Kudos
KenGorton
Esri Contributor

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

0 Kudos
nakulmanocha
Esri Regular Contributor

I am glad I was able to help you to resolve this issue. 

Cheers,

Nakul

0 Kudos