Download MMPK (not PrePlannedMapArea)

368
7
Jump to solution
09-02-2021 02:22 PM
RTC
by
New Contributor II

I have several MMPK maps on AGOL that I want to download into my app (not side load).

What are the proper steps to get the mmpk file downloaded into the app?

Tags (2)
0 Kudos
2 Solutions

Accepted Solutions
Nicholas-Furness
Esri Regular Contributor

Hi.

I expect that the AGSPortalItem is going out of scope and is being released before fetchData completes and calls into the completion block. Try keeping a strong reference to the AGSPortalItem (maybe make it a var on your class instance). You can nil it out from within the completion block if you need to.

View solution in original post

0 Kudos
Nicholas-Furness
Esri Regular Contributor

There is another way to download the MMPK. You can use AGSRequestOperation. The advantage here is that you have access to the download progress, and you can also avoid having to create a large Data object in memory. The AGSRequestOperation can stream the download directly to your file. You would do something like this:

func downloadMMPK(from portalItemForMMPK: AGSPortalItem) {
    
    portalItemForMMPK.load { [weak self] error in
        
        guard let self = self else { return }
        
        if let error = error {
            print("Unable to load the portal item: \(error.localizedDescription)")
            return
        }
        
        guard let portalUrl = portalItemForMMPK.portal.url,
              let itemDataUrl = URL(string: "\(portalUrl)/sharing/rest/content/items/\(portalItemForMMPK.itemID)/data"),
              let docsFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
        
        let mmpkLocation = docsFolder.appendingPathComponent(portalItemForMMPK.name, isDirectory: false)
        
        let request = AGSRequestOperation(url: itemDataUrl)
        
        request.outputFileURL = mmpkLocation
        request.registerListener(self) { result, error in
            if let error = error {
                print("Unable to download the file: \(error.localizedDescription)")
                return
            }
            
            guard let result = result as? URL,
                  result.isFileURL else { return }
            
            let mmpk = AGSMobileMapPackage(fileURL: result)
            mmpk.load { error in
                if let error = error {
                    print("Unable to open the mmpk file: \(error.localizedDescription)")
                    return
                }
                
                if let mapFromMMPK = mmpk.maps.first {
                    self.mapView.map = mapFromMMPK
                }
            }
        }
        
        request.progressHandler = { (downloaded, total) in
            print("Downloaded \(100 * downloaded/total)%. \(total - downloaded) bytes remaining")
        }

        self.progressView.observedProgress = request.progress
        
        AGSOperationQueue.shared().addOperation(request)
    }
}

 

 

Note, in that code, I've added a ProgressView to my ViewController. I'm actually showing progress twice - once printed to the console in the AGSRequestOperation's progressHandler, and at the same time by just having my UI's ProgressView lean on the AGSRequestOperation's progress object.

The key is defining the outputFileURL property on the AGSRequestOperation. If you do that, the registered listener will be passed a URL object, otherwise it'll get a Data object.

View solution in original post

0 Kudos
7 Replies
Nicholas-Furness
Esri Regular Contributor

You should be able to create an AGSPortalItem for the AGOL item for the MMPK, and then call fetchData() on that to get NSData. You can then call NSData.writeToFile() to write that MMPK to your iOS filesystem as a .mmpk file.

0 Kudos
RTC
by
New Contributor II

Thank you Nicholas, I will give that a try.

0 Kudos
RTC
by
New Contributor II

Hello Nicholas,

I tried to load the portalItem as per your instructions but can't seem to get the portal data to load. When I call the fetchData, noting happens. I can see in the AGS logs that the request get sent but there is never a response. No errors, no data, nothing. Here is how I am calling fetchData, do you see any issues? Thanks for your assistance!

 

 

// tried this way
let portalItem = AGSPortalItem(url: portal, itemID: map.itemId)
// also tried this way
let portalItem = AGSPortalItem(url: myURL)

// load portalItem
portalItem.load {error in
            if let error = error {
                print("Error load mobile map package (mmpk) failed: \(error)")
                return
            }

            if portalItem.type == .mobileMapPackage {
                print("MMPK load completed")

// fetch portal data (This never returns, no error, no data, no response)
                portalItem.fetchData { data, error in
                    if let error = error {
                        print("Error fetch data mobile map package failed: \(error)")
                        return
                    }

                    if data == nil {
                        print("Error MMPK fetch failed: no data")
                    } else {
                        print("MMPK fetch data mobile map package completed")
                        do {
                            try data?.write(to: downloadDirectory)
                            print("MMPK write data mobile map package completed")
                        } catch {
                            print("Error MMPK write data failed: \(error.localizedDescription)")
                        }
                    }
                }

            } else {
                print("Error MMPK load mobile map package (mmpk) failed")
            }

        }

 

 

0 Kudos
Nicholas-Furness
Esri Regular Contributor

Hi.

I expect that the AGSPortalItem is going out of scope and is being released before fetchData completes and calls into the completion block. Try keeping a strong reference to the AGSPortalItem (maybe make it a var on your class instance). You can nil it out from within the completion block if you need to.

View solution in original post

0 Kudos
Nicholas-Furness
Esri Regular Contributor

There is another way to download the MMPK. You can use AGSRequestOperation. The advantage here is that you have access to the download progress, and you can also avoid having to create a large Data object in memory. The AGSRequestOperation can stream the download directly to your file. You would do something like this:

func downloadMMPK(from portalItemForMMPK: AGSPortalItem) {
    
    portalItemForMMPK.load { [weak self] error in
        
        guard let self = self else { return }
        
        if let error = error {
            print("Unable to load the portal item: \(error.localizedDescription)")
            return
        }
        
        guard let portalUrl = portalItemForMMPK.portal.url,
              let itemDataUrl = URL(string: "\(portalUrl)/sharing/rest/content/items/\(portalItemForMMPK.itemID)/data"),
              let docsFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
        
        let mmpkLocation = docsFolder.appendingPathComponent(portalItemForMMPK.name, isDirectory: false)
        
        let request = AGSRequestOperation(url: itemDataUrl)
        
        request.outputFileURL = mmpkLocation
        request.registerListener(self) { result, error in
            if let error = error {
                print("Unable to download the file: \(error.localizedDescription)")
                return
            }
            
            guard let result = result as? URL,
                  result.isFileURL else { return }
            
            let mmpk = AGSMobileMapPackage(fileURL: result)
            mmpk.load { error in
                if let error = error {
                    print("Unable to open the mmpk file: \(error.localizedDescription)")
                    return
                }
                
                if let mapFromMMPK = mmpk.maps.first {
                    self.mapView.map = mapFromMMPK
                }
            }
        }
        
        request.progressHandler = { (downloaded, total) in
            print("Downloaded \(100 * downloaded/total)%. \(total - downloaded) bytes remaining")
        }

        self.progressView.observedProgress = request.progress
        
        AGSOperationQueue.shared().addOperation(request)
    }
}

 

 

Note, in that code, I've added a ProgressView to my ViewController. I'm actually showing progress twice - once printed to the console in the AGSRequestOperation's progressHandler, and at the same time by just having my UI's ProgressView lean on the AGSRequestOperation's progress object.

The key is defining the outputFileURL property on the AGSRequestOperation. If you do that, the registered listener will be passed a URL object, otherwise it'll get a Data object.

View solution in original post

0 Kudos
RTC
by
New Contributor II

Excellent, I will give that a try, thanks!

0 Kudos
RTC
by
New Contributor II

Thanks Nicholas, I think this is the best solution for downloading mmpk files to an iOS app.

0 Kudos