Unable to take web map offline: "The feature service does not support geodatabase sync"

1658
6
Jump to solution
01-06-2020 09:34 AM
CoryDavis
New Contributor III

I'm working on enabling offline sync in an iOS app using ArcGIS runtime 100.6. The back end is ArcGIS Enterprise 10.7.1.

I created a web map which only uses the World Topography Map (for Export) as a basemap and a single feature service created by ESRI to as a controlled test to guard against any mistakes I could have made while publishing the services which the production app will need to consume. These should present no problem being taken offline, but this error is generated after I initiate the download:

"The feature service does not support geodatabase sync"

I checked the web map settings on portal. It is shared with everyone and the "offline mode" switch is enabled, so I don't understand how I could be getting this error. To clarify, the basemap loads successfully, but the feature layer does not.

The code for initiating offline mode doesn't deviate much from the sample in this article (The function "downloadGeodatabaseForLayers() follows this article😞

    func takeMapOffline(areaOfInterest: AGSGeometry) {
        //1. Declare offline map task
        //let offlineMapTask = AGSOfflineMapTask(onlineMap: self.mapView.map!)
        let offlineMap = AGSMap.init(url: URL(string: self.offlineWebMapUrl3)!)
        let offlineMapTask = AGSOfflineMapTask(onlineMap: offlineMap!)
        
        //2. Define parameters
        offlineMapTask.defaultGenerateOfflineMapParameters(withAreaOfInterest: areaOfInterest, completion: {(parameters, error) in
            print("@@@ Generate parameters 1: Start")
            if let error = error {
                print("@@@ Generate parameters 2b: Error, getting parameters failed")
                print(error)
                self.showDownloadFailedAlert("Error generating offline parameters: \(error.localizedDescription)")
                return
            }
            guard parameters != nil else {
                print("takeMapOffline: No parameters provided")
                return
            }
            if let parameters = parameters {
                parameters.maxScale = 3600
                parameters.minScale = 30000
                parameters.includeBasemap = true
                parameters.isDefinitionExpressionFilterEnabled = true
                parameters.continueOnErrors = true
                parameters.returnSchemaOnlyForEditableLayers = true
                parameters.attachmentSyncDirection = .bidirectional
                parameters.returnLayerAttachmentOption = .allLayers
                print("@@@ Generate parameters 2a: Got parameters!")
                
                //4. Create the job
                print("@@@ Generate parameters 3: create job")
                let mapStorageURL = self.getNewOfflineMapDirectoryURL()
                UserDefaults.standard.set(mapStorageURL, forKey: "OfflineMapDirectoryUrl")
                let AGSOfflineMapJob = offlineMapTask.generateOfflineMapJob(with: parameters, downloadDirectory: mapStorageURL)
                
                //5. Run the job
                self.startOfflineMapJob(job: AGSOfflineMapJob, areaOfInterest: areaOfInterest)
            }
        })
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍


    func startOfflineMapJob(job: AGSGenerateOfflineMapJob, areaOfInterest: AGSGeometry) {
        job.start(statusHandler: { (status) in
            DispatchQueue.main.async {
                //Add canceling logic to SVProgressHUD
                
                SVProgressHUD.showDismissable(with: Float(job.progress.fractionCompleted), status: "Downloading map selection. This may take several minutes. Tap here to cancel.")
            }
            print("Status [\(String(describing: job.progress.fractionCompleted))]: \(status)")
            if job.status == .failed {
                DispatchQueue.main.async {
                    SVProgressHUD.dismiss()
                }
                self.showDownloadFailedAlert("Server Error")
                return
            }
        }) { (result, error) in
            DispatchQueue.main.async {
                SVProgressHUD.dismiss()
            }
            if let error = error {
                print(error)
                self.showDownloadFailedAlert(error.localizedDescription)
                return
            }
            guard let result = result else {
                self.showDownloadFailedAlert("Didn't receive a response.")
                return
                
            }
            if result.hasErrors {
                result.layerErrors.forEach{(layerError) in
                    print((layerError.key.name), " Error taking this layer offline: ", (layerError.value))
                }
                result.tableErrors.forEach{(tableError) in
                    print((tableError.key.tableName), " Error taking this table offline")
                }
                self.showDownloadFailedAlert("Error taking layers offline.")
            }
            else {
                //display the offline map
                self.mapView.map = result.offlineMap
                
                DispatchQueue.main.async {
                    self.downloadGeodatabaseForLayers(areaOfInterest: areaOfInterest)
                }
            }
        }
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

    func downloadGeodatabaseForLayers(areaOfInterest: AGSGeometry) {
        print("@@@Downloading geodatabase")
        let syncTask = AGSGeodatabaseSyncTask(url: URL(string: testOfflineServiceUrl)!)
        syncTask.defaultGenerateGeodatabaseParameters(withExtent: areaOfInterest, completion: {[weak self] (parameters, error) in
            if let error = error {
                print("@@@Error generating geodatabase:" + error.localizedDescription)
                print(error)
                return
            }
            guard parameters != nil else {
                print("@@@No parameters")
                return
            }
            if let parameters = parameters {
                parameters.syncModel = .layer
                // define the layers and features to include
                parameters.layerOptions.removeAll()
                let layersToInclude = [0]
                for val in layersToInclude {
                parameters.layerOptions.append(AGSGenerateLayerOption(layerID: val, includeRelated: true))
                }
                parameters.returnAttachments = false
                //Start the download task
                let geodatabaseUrl = self?.getNewOfflineMapDirectoryURL().appendingPathExtension(".geodatabase")
                
                let generateJob = syncTask.generateJob(with: parameters, downloadFileURL: (geodatabaseUrl)!)
                generateJob.start(statusHandler: { (status) in
                   DispatchQueue.main.async {
                    SVProgressHUD.showProgress(Float(generateJob.progress.fractionCompleted), status: "Downloading Geodatabase")
                    }
                 }, completion: { (geodatabase, error) in
                    SVProgressHUD.dismiss()
                    if let error = error {
                        print("@@@Error downloading geodatabase: ")
                        print(error)
                        return
                    }
                    else if let geoDb = geodatabase {
                        self!.generatedGeodatabase = geoDb
                        self!.displayLayersFromGeodatabase(geoDb: geoDb)
                    }
                })
                
            }
            
        })
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

If I download the app container, I can see both the mmpk file and the geodatabase file, but only the basemap displays. Can someone suggest a solution?

0 Kudos
1 Solution

Accepted Solutions
JoeHershman
MVP Regular Contributor

There is no reason to call the downloadGeodatabaseForLayers.  Basically you are taking the map offline which will create a package that includes the tiles from the basemap and the data from the service (inside the defined boundary).

Inside the setup of taking the the takeMapOffline you define the parameter: 

parameters.returnSchemaOnlyForEditableLayers = true

So you are not returning data from the feature service.

Not 100% sure on your error, unless possibly testOfflineServiceUrl may not be correct as Cory Davis‌ mentioned

Thanks,
-Joe

View solution in original post

6 Replies
MaryHarvey
Esri Contributor

Dear Cory,

Thank you for submitting this question. I cannot immediately spot any problem with the code. I am unclear, however, as to why you are calling the downloadGeodatabaseForLayers function in order to take a map offline. You should be able to take the map offline without calling your function 'downloadGeodatabaseForLayers'.

Can you confirm where the error message is triggered from? Is it from the AGSGenerateOfflineMapJob.Start or from the downloadGeodatabaseForLayers function?

Also, what is the URL for the 'testOfflineServiceURL' that you are passing to the 

let syncTask = AGSGeodatabaseSyncTask(url: URL(string: testOfflineServiceUrl)!)

Thanks,

Mary

0 Kudos
CoryDavis
New Contributor III

Mary,

The error is triggered from downloadGeodatabaseForLayers. I added this function at the suggestion of an ESRI tech I worked with over the phone. I noticed the geodatabase was being downloaded before I added it (I could see the file in the app container), but none of the features I selected with the areaOfInterest geometry appeared on the downloaded map.

The value of testOfflineServiceUrl is Layer: Wildfire Response Points (ID: 0) 

0 Kudos
Nicholas-Furness
Esri Regular Contributor

Hi Cory,

As Mary and Joe pointed out, you shouldn't have to call downloadGeodatabaseForLayers and you shouldn't set returnSchemaOnlyForEditableLayer to true (that's why you're not getting any data).

There's a more fundamental issue which is, I think, why you might be seeing an error. You are not holding on to the OfflineMapTask, nor the Job. Not entirely sure why that would give you the error you're seeing but I suspect something is bubbling up internally and being caught by code that expects something specific.

Anyway, if you provide a class level var to hold the AGSOfflineMapTask and the AGSGenerateOfflineMapJob (see lines 34 and 35 below), and you remove the line that sets returnSchemaForEditableLayers, your code works:

import UIKit
import ArcGIS

class ViewController: UIViewController {
    
    @IBOutlet weak var mapView: AGSMapView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        mapView.map = AGSMap(url: URL(string: "https://geeknixta.maps.arcgis.com/home/item.html?id=8e88bb4d6cc048cbb61686fbfb26a0b9")!)
        
        mapView.touchDelegate = self
    }

    var offlineMapTask: AGSOfflineMapTask!
    var offlineMapJob: AGSGenerateOfflineMapJob!
    
    func takeMapOffline(areaOfInterest: AGSGeometry) {
        //1. Declare offline map task
        //let offlineMapTask = AGSOfflineMapTask(onlineMap: self.mapView.map!)
        guard let onlineMap = mapView.map else { return }
        
        // IMPORTANT: hold on to the Task while it executes
        offlineMapTask = AGSOfflineMapTask(onlineMap: onlineMap)
        
        //2. Define parameters
        offlineMapTask.defaultGenerateOfflineMapParameters(withAreaOfInterest: areaOfInterest, completion: { [weak self] (parameters, error) in
            
            // IMPORTANT: If we hadn't held on to the task above, this is at risk of not executing.
            // However, because offlineMapTask was referenced below when creating the job,
            // Swift would have kept it around, but it's better practice to keep hold of it just in case.
            
            print("@@@ Generate parameters 1: Start")
            if let error = error {
                print("@@@ Generate parameters 2b: Error, getting parameters failed")
                print(error)
                self?.showDownloadFailedAlert("Error generating offline parameters: \(error.localizedDescription)")
                return
            }
            guard parameters != nil else {
                print("takeMapOffline: No parameters provided")
                return
            }
            
            guard let self = self else { return }
            
            if let parameters = parameters {
                parameters.maxScale = 3600
                parameters.minScale = 30000
                parameters.includeBasemap = true
                parameters.isDefinitionExpressionFilterEnabled = true
                parameters.continueOnErrors = true
//                parameters.returnSchemaOnlyForEditableLayers = true
                parameters.attachmentSyncDirection = .bidirectional
                parameters.returnLayerAttachmentOption = .allLayers
                print("@@@ Generate parameters 2a: Got parameters!")
                
                //4. Create the job
                print("@@@ Generate parameters 3: create job")
                let mapStorageURL = self.getNewOfflineMapDirectoryURL()
                UserDefaults.standard.set(mapStorageURL, forKey: "OfflineMapDirectoryUrl")
                
                // IMPORTANT: Hold on to the Job while it executes. This is where the original code was
                // exposing itself to not completing properly.
                self.offlineMapJob = self.offlineMapTask.generateOfflineMapJob(with: parameters, downloadDirectory: mapStorageURL)
                
                //5. Run the job
                self.startOfflineMapJob(job: self.offlineMapJob, areaOfInterest: areaOfInterest)
            }
        })
    }
    
    var downloadProgressObserver: NSKeyValueObservation?
    
    func startOfflineMapJob(job: AGSGenerateOfflineMapJob, areaOfInterest: AGSGeometry) {
        
        downloadProgressObserver = job.progress.observe(\.fractionCompleted, changeHandler: { (progress, _) in
            DispatchQueue.main.async {
                print("Progress at \(progress.fractionCompleted * 100)%")
            }
        })
        
        print("Job Started: Downloading map selection. This may take several minutes. Tap here to cancel.")

        job.start(statusHandler: { (status) in
            print("Status [\(String(describing: job.progress.fractionCompleted))]: \(status)")
            // You really want to wait for the error to come through the completion
            // block rather than checking on status updates.
            if job.status == .failed {
                DispatchQueue.main.async {
                    //                        SVProgressHUD.dismiss()
                    print("Progress Done")
                }
                self.showDownloadFailedAlert("Server Error")
                return
            }
        }) { (result, error) in
            DispatchQueue.main.async {
                //                    SVProgressHUD.dismiss()
                print("Progress Done")
            }
            if let error = error {
                print(error)
                self.showDownloadFailedAlert(error.localizedDescription)
                return
            }
            guard let result = result else {
                self.showDownloadFailedAlert("Didn't receive a response.")
                return
                
            }
            if result.hasErrors {
                result.layerErrors.forEach{(layerError) in
                    print((layerError.key.name), " Error taking this layer offline: ", (layerError.value))
                }
                result.tableErrors.forEach{(tableError) in
                    print((tableError.key.tableName), " Error taking this table offline")
                }
                self.showDownloadFailedAlert("Error taking layers offline.")
            }
            else {
                //display the offline map
                self.mapView.map = result.offlineMap
                
                for layer in result.offlineMap.operationalLayers {
                    print(layer)
                }
            }
        }
    }
}

extension ViewController: AGSGeoViewTouchDelegate {
    func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
        guard let areaOfInterest = mapView.currentViewpoint(with: .boundingGeometry)?.targetGeometry else { return }
        takeMapOffline(areaOfInterest: areaOfInterest)
    }
    
    func showDownloadFailedAlert(_ message: String) {
        print(message)
    }
    
    func getNewOfflineMapDirectoryURL() -> URL {
        FileManager.default.getNewOfflineMapDirectoryURL()
    }
}

extension FileManager {
    private var onDemandMapsDirectory: URL {
        get {
            let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
            let onDemandDirectoryURL = documentDirectoryURL.appendingPathComponent("on-demand-maps")
            return onDemandDirectoryURL
        }
    }
    
    func getNewOfflineMapDirectoryURL() -> URL {
        try? FileManager.default.createDirectory(at: onDemandMapsDirectory, withIntermediateDirectories: true, attributes: nil)
        return onDemandMapsDirectory.appendingPathComponent("\(ISO8601DateFormatter().string(from: Date()))")
    }
    
    func clearUpOfflineMaps() {
        try? FileManager.default.contentsOfDirectory(at: onDemandMapsDirectory, includingPropertiesForKeys: nil, options: FileManager.DirectoryEnumerationOptions.skipsSubdirectoryDescendants).forEach {
            do {
                try FileManager.default.removeItem(at: $0)
                print("Deleted '\($0)'")
            } catch {
                print("Could not delete '\($0)': \(error.localizedDescription)")
            }
        }
    }
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
0 Kudos
JoeHershman
MVP Regular Contributor

There is no reason to call the downloadGeodatabaseForLayers.  Basically you are taking the map offline which will create a package that includes the tiles from the basemap and the data from the service (inside the defined boundary).

Inside the setup of taking the the takeMapOffline you define the parameter: 

parameters.returnSchemaOnlyForEditableLayers = true

So you are not returning data from the feature service.

Not 100% sure on your error, unless possibly testOfflineServiceUrl may not be correct as Cory Davis‌ mentioned

Thanks,
-Joe
JoeHershman
MVP Regular Contributor

Sorry mixed up my names in the above post....

Thanks,
-Joe
0 Kudos
CoryDavis
New Contributor III

Thanks so much, this is exactly what I needed. Works great now!

0 Kudos