diff --git a/arcgis-runtime-samples-macos.xcodeproj/project.pbxproj b/arcgis-runtime-samples-macos.xcodeproj/project.pbxproj index 441897f..b0e5984 100644 --- a/arcgis-runtime-samples-macos.xcodeproj/project.pbxproj +++ b/arcgis-runtime-samples-macos.xcodeproj/project.pbxproj @@ -389,6 +389,9 @@ 97F8BF191FDA6FB400A65B2E /* RasterLayerGPKGViewController.swift in CopyFiles */ = {isa = PBXBuildFile; fileRef = 97F8BF151FDA6E6300A65B2E /* RasterLayerGPKGViewController.swift */; }; C700D3C6216BF2E100E365B5 /* ProgressViewController.swift in CopyFiles */ = {isa = PBXBuildFile; fileRef = C7D22F892167D35200C39D5C /* ProgressViewController.swift */; }; C7052B1E216E9130002F077F /* ProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C7052B1D216E9130002F077F /* ProgressView.xib */; }; + C70905DC215EBFDE00689C24 /* DownloadPreplannedMap.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C70905D9215EBFDE00689C24 /* DownloadPreplannedMap.storyboard */; }; + C70905DE215EBFDE00689C24 /* DownloadPreplannedMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70905DB215EBFDE00689C24 /* DownloadPreplannedMapViewController.swift */; }; + C70905DF215EC58A00689C24 /* DownloadPreplannedMapViewController.swift in CopyFiles */ = {isa = PBXBuildFile; fileRef = C70905DB215EBFDE00689C24 /* DownloadPreplannedMapViewController.swift */; }; C7197AEB218A278300838D31 /* OpenMobileMap.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C7197AE9218A278300838D31 /* OpenMobileMap.storyboard */; }; C7197AEC218A278300838D31 /* OpenMobileMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7197AEA218A278300838D31 /* OpenMobileMapViewController.swift */; }; C7197AED218A278F00838D31 /* OpenMobileMapViewController.swift in CopyFiles */ = {isa = PBXBuildFile; fileRef = C7197AEA218A278300838D31 /* OpenMobileMapViewController.swift */; }; @@ -505,6 +508,7 @@ C700D3C6216BF2E100E365B5 /* ProgressViewController.swift in CopyFiles */, C7D22F882167BDE300C39D5C /* UseGeodatabaseTransactionsViewController.swift in CopyFiles */, C729FC352159512F00D42EA7 /* ListKMLContentsViewController.swift in CopyFiles */, + C70905DF215EC58A00689C24 /* DownloadPreplannedMapViewController.swift in CopyFiles */, C796FAF321B8929D00B5849E /* UpdateAttributesViewController.swift in CopyFiles */, C743A8F7215A9D550081B756 /* DisplayKMLNetworkLinksViewController.swift in CopyFiles */, C731A01F21B734EA003F5D14 /* BufferViewController.swift in CopyFiles */, @@ -924,6 +928,8 @@ 97F8BF141FDA6E6300A65B2E /* RasterLayerGPKG.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = RasterLayerGPKG.storyboard; path = "arcgis-runtime-samples-macos/Layers/Raster layer (geopackage)/RasterLayerGPKG.storyboard"; sourceTree = SOURCE_ROOT; }; 97F8BF151FDA6E6300A65B2E /* RasterLayerGPKGViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RasterLayerGPKGViewController.swift; path = "arcgis-runtime-samples-macos/Layers/Raster layer (geopackage)/RasterLayerGPKGViewController.swift"; sourceTree = SOURCE_ROOT; }; C7052B1D216E9130002F077F /* ProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProgressView.xib; sourceTree = ""; }; + C70905D9215EBFDE00689C24 /* DownloadPreplannedMap.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DownloadPreplannedMap.storyboard; sourceTree = ""; }; + C70905DB215EBFDE00689C24 /* DownloadPreplannedMapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreplannedMapViewController.swift; sourceTree = ""; }; C7197AE9218A278300838D31 /* OpenMobileMap.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = OpenMobileMap.storyboard; sourceTree = ""; }; C7197AEA218A278300838D31 /* OpenMobileMapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenMobileMapViewController.swift; sourceTree = ""; }; C730BFD421B5CC610076FC18 /* Buffer.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Buffer.storyboard; sourceTree = ""; }; @@ -1539,6 +1545,7 @@ 3E496EC81DD14FE2004C921C /* Mobile map (search and route) */, C7E79ADC213F3B4800C24A02 /* Generate offline map */, C7586C812153F6DD005A4F11 /* Generate offline map (overrides) */, + C70905D8215EBE5600689C24 /* Download preplanned map */, D959FE3D1FDC771400B507CB /* Read a geopackage */, ); path = Maps; @@ -2338,6 +2345,15 @@ path = "Progress View"; sourceTree = ""; }; + C70905D8215EBE5600689C24 /* Download preplanned map */ = { + isa = PBXGroup; + children = ( + C70905D9215EBFDE00689C24 /* DownloadPreplannedMap.storyboard */, + C70905DB215EBFDE00689C24 /* DownloadPreplannedMapViewController.swift */, + ); + path = "Download preplanned map"; + sourceTree = ""; + }; C7197AE8218A26DA00838D31 /* Open mobile map (map package) */ = { isa = PBXGroup; children = ( @@ -2838,6 +2854,7 @@ 3E496EF21DD39DF4004C921C /* RouteAroundBarriers.storyboard in Resources */, 3EFDE0851E30100800CBCD92 /* ShowLegend.storyboard in Resources */, 3E0103721E298D300013AEEF /* streetmap_SD.tpk in Resources */, + C70905DC215EBFDE00689C24 /* DownloadPreplannedMap.storyboard in Resources */, 3EFDE0991E303B0600CBCD92 /* srtm.tiff.ovr in Resources */, 3EFDE0B71E31287B00CBCD92 /* Shasta_Elevation.tif.aux.xml in Resources */, 10C3F21F1F02BBF5003C7F20 /* TerrainExaggeration.storyboard in Resources */, @@ -3018,6 +3035,7 @@ C764E0FD2171438C00A82795 /* ShowLabelsOnLayersViewController.swift in Sources */, 3EF190AA1EF9BF550037779D /* AddDeleteRelatedFeaturesVC.swift in Sources */, 3ECCFE321BCC4FC900CE256D /* ShowMagnifierViewController.swift in Sources */, + C70905DE215EBFDE00689C24 /* DownloadPreplannedMapViewController.swift in Sources */, 4CB1707221668F480038535D /* CollectionViewItem.swift in Sources */, 3E84EE461DCA4ADE006731B5 /* FeatureTemplatePickerVC.swift in Sources */, 3EC1AE6F1BAC8DA100E32B1D /* MainViewController.swift in Sources */, diff --git a/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/Contents.json b/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/Contents.json new file mode 100644 index 0000000..a6f8f51 --- /dev/null +++ b/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "download preplanned map.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "download preplanned map@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "download preplanned map@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map.png b/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map.png new file mode 100644 index 0000000..8454fc2 Binary files /dev/null and b/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map.png differ diff --git a/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map@2x.png b/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map@2x.png new file mode 100644 index 0000000..d20d0ea Binary files /dev/null and b/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map@2x.png differ diff --git a/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map@3x.png b/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map@3x.png new file mode 100644 index 0000000..554ed2e Binary files /dev/null and b/arcgis-runtime-samples-macos/Assets.xcassets/Sample icons/Download preplanned map.imageset/download preplanned map@3x.png differ diff --git a/arcgis-runtime-samples-macos/Content Display Logic/ContentPList.plist b/arcgis-runtime-samples-macos/Content Display Logic/ContentPList.plist index 565a9b6..c2c9568 100644 --- a/arcgis-runtime-samples-macos/Content Display Logic/ContentPList.plist +++ b/arcgis-runtime-samples-macos/Content Display Logic/ContentPList.plist @@ -269,6 +269,19 @@ ProgressViewController + + displayName + Download preplanned map + storyboardName + DownloadPreplannedMap + descriptionText + List and download preplanned map areas from a webmap. + sourceFileNames + + DownloadPreplannedMapViewController + ProgressViewController + + displayName Read a geopackage diff --git a/arcgis-runtime-samples-macos/Maps/Download preplanned map/DownloadPreplannedMap.storyboard b/arcgis-runtime-samples-macos/Maps/Download preplanned map/DownloadPreplannedMap.storyboard new file mode 100644 index 0000000..703fc39 --- /dev/null +++ b/arcgis-runtime-samples-macos/Maps/Download preplanned map/DownloadPreplannedMap.storyboard @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/arcgis-runtime-samples-macos/Maps/Download preplanned map/DownloadPreplannedMapViewController.swift b/arcgis-runtime-samples-macos/Maps/Download preplanned map/DownloadPreplannedMapViewController.swift new file mode 100644 index 0000000..81be392 --- /dev/null +++ b/arcgis-runtime-samples-macos/Maps/Download preplanned map/DownloadPreplannedMapViewController.swift @@ -0,0 +1,249 @@ +// +// Copyright 2018 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. +// + +import AppKit +import ArcGIS + +class DownloadPreplannedMapViewController: NSViewController, AGSAuthenticationManagerDelegate { + + @IBOutlet weak var mapView: AGSMapView! + + @IBOutlet weak var popUpButton: NSPopUpButton! + @IBOutlet weak var removeDownloadsButton: NSButton! + + private var offlineMapTask: AGSOfflineMapTask? + private var downloadPreplannedMapJob: AGSDownloadPreplannedOfflineMapJob? + private var portalItem: AGSPortalItem? + private var shouldShowAlert = true + + var localAreaURLs: [String: URL] = [:] + + override func viewDidLoad() { + super.viewDidLoad() + + //prepare the authentication manager for user login (required for downloading the sample's basemap) + //let config = AGSOAuthConfiguration(portalURL: nil, clientID: "vVHDSfKfdKBs8lkA", redirectURL: nil) + //AGSAuthenticationManager.shared().oAuthConfigurations.add(config) + //AGSAuthenticationManager.shared().credentialCache.removeAllCredentials() + } + + override func viewDidAppear() { + super.viewDidAppear() + + //show the login alert if this is the first time appearing + /* if shouldShowAlert { + shouldShowAlert = false + showLoginQueryAlert() + }*/ + loadMap() + } + + private func showLoginQueryAlert() { + let alert = NSAlert() + alert.messageText = "This sample requires you to login in order to take the map's basemap offline. Would you like to continue?" + alert.addButton(withTitle: "Login") + alert.addButton(withTitle: "Cancel") + alert.beginSheetModal(for: view.window!) { [weak self] (response) in + if response == .alertFirstButtonReturn { + self?.loadMap() + } + } + } + + private func loadMap() { + + //portal for the web map + let portal = AGSPortal.arcGISOnline(withLoginRequired: false) + + //portal item for web map + let portalItem = AGSPortalItem(portal: portal, itemID: "acc027394bc84c2fb04d1ed317aac674") + self.portalItem = portalItem + + loadMapForPortalItem() + + //load the map asynchronously + mapView.map?.load { [weak self] (error) in + + guard let self = self else { + return + } + + if let error = error { + //if not user cancelled + if (error as NSError).code != NSUserCancelledError { + //display error as alert + NSAlert(error: error).beginSheetModal(for: self.view.window!) + } + } + + } + + //instantiate offline map task + let offlineMapTask = AGSOfflineMapTask(portalItem: portalItem) + self.offlineMapTask = offlineMapTask + offlineMapTask.getPreplannedMapAreas(completion: { [weak self] (preplannedMapAreas, error) in + guard let self = self else { + return + } + guard error == nil else { + NSAlert(error: error!).beginSheetModal(for: self.view.window!) + return + } + guard let preplannedMapAreas = preplannedMapAreas else { + return + } + for area in preplannedMapAreas { + let menuItem = NSMenuItem() + menuItem.title = area.portalItem!.title + menuItem.representedObject = area + menuItem.indentationLevel = 1 + self.popUpButton.menu?.addItem(menuItem) + } + self.popUpButton.isEnabled = true + self.view.layout() + }) + } + + private func loadMapForPortalItem() { + if let portalItem = portalItem { + //map from portal item + let map = AGSMap(item: portalItem) + //assign map to the map view + mapView.map = map + } + } + + // MARK: - Preplanned map download + + func downloadPreplannedMapArea(_ preplannedMapArea: AGSPreplannedMapArea) { + + guard let offlineMapTask = offlineMapTask else { + return + } + + let directorURL = preplannedMapLocalURL(for: preplannedMapArea) + + guard !FileManager.default.fileExists(atPath: directorURL.path) else { + loadDownloadedMap(at: directorURL) + return + } + + let downloadPreplannedMapJob = offlineMapTask.downloadPreplannedOfflineMapJob(with: preplannedMapArea, + downloadDirectory: directorURL) + self.downloadPreplannedMapJob = downloadPreplannedMapJob + + //show the progress sheet + let progressController = ProgressViewController(progress: downloadPreplannedMapJob.progress, operationLabel: "Downloading Preplanned Map Area") + presentAsSheet(progressController) + + //start the job + downloadPreplannedMapJob.start(statusHandler: nil) { [weak self] (result: AGSDownloadPreplannedOfflineMapResult?, error: Error?) in + + //close the progress sheet since the job is no longer active + progressController.dismiss(self) + + guard let self = self else { + return + } + + if let error = error { + //if not user cancelled + if (error as NSError).code != NSUserCancelledError, + let window = self.view.window { + //display error as alert + NSAlert(error: error).beginSheetModal(for: window) + } + } else if let result = result { + self.preplannedMapDownloadDidSucceed(with: result) + } + } + } + + private func loadDownloadedMap(at url: URL) { + let package = AGSMobileMapPackage(fileURL: url) + package.load { [weak self] (error) in + if error == nil, + let map = package.maps.first { + self?.mapView.map = map + } + } + } + + /// Called when a preplanned map downloads successfully. + /// + /// - Parameter result: The result of the download preplanned map job. + private func preplannedMapDownloadDidSucceed(with result: AGSDownloadPreplannedOfflineMapResult) { + // Show any layer or table errors to the user. + if let layerErrors = result.layerErrors as? [AGSLayer: Error], + let tableErrors = result.tableErrors as? [AGSFeatureTable: Error], + !(layerErrors.isEmpty && tableErrors.isEmpty) { + + let errorMessages = layerErrors.map { "\($0.key.name): \($0.value.localizedDescription)" } + + tableErrors.map { "\($0.key.displayName): \($0.value.localizedDescription)" } + let alert = NSAlert() + alert.messageText = "Offline Map Generated with Errors" + alert.informativeText = "The following error(s) occurred while generating the offline map:\n\n\(errorMessages.joined(separator: "\n"))" + alert.beginSheetModal(for: view.window!) + } + + //assign offline map to map view + mapView.map = result.offlineMap + + removeDownloadsButton.isEnabled = true + } + + // MARK: - Actions + + @IBAction func popUpButtonAction(_ sender: NSPopUpButton) { + if let selectedItem = sender.selectedItem, + let preplannedMapArea = selectedItem.representedObject as? AGSPreplannedMapArea { + downloadPreplannedMapArea(preplannedMapArea) + } else { + loadMapForPortalItem() + } + } + + @IBAction func removeDownloadsButtonAction(_ sender: NSButton) { + for url in localAreaURLs.values { + try? FileManager.default.removeItem(at: url) + } + localAreaURLs = [:] + loadMapForPortalItem() + popUpButton.selectItem(at: 0) + sender.isEnabled = false + } + + // MARK: - Helper methods + + private func preplannedMapLocalURL(for preplannedMapArea: AGSPreplannedMapArea) -> URL { + + let areaID = preplannedMapArea.portalItem!.itemID + + if let url = localAreaURLs[areaID] { + return url + } else { + //get a suitable directory to place files + let directoryURL = FileManager.default.temporaryDirectory + + //create a unique name for the geodatabase based on current timestamp + let formattedDate = ISO8601DateFormatter().string(from: Date()) + let url = directoryURL.appendingPathComponent("preplanned-map-\(formattedDate).geodatabase") + localAreaURLs[areaID] = url + return url + } + } + +} diff --git a/arcgis-runtime-samples-macos/Maps/Download preplanned map/README.md b/arcgis-runtime-samples-macos/Maps/Download preplanned map/README.md new file mode 100644 index 0000000..7f0f467 --- /dev/null +++ b/arcgis-runtime-samples-macos/Maps/Download preplanned map/README.md @@ -0,0 +1,25 @@ +# Download Preplanned Map + +This sample demonstrates how to download preplanned map areas from a webmap. In the preplanned offline workflow, the author of the online map defines map areas for offline use. When these areas are created, their offline packages are created and stored online for clients to download. + +![Download Preplanned Map Screenshot](image1.png) + +## How to use the sample + +Select a preplanned map area from the popup button. A sheet appears showing the download progress. Once downloaded, the preplanned map is displayed in the map view. If a preplanned map is reselected later, the locally cached data is loaded immediately. + +Click the "Remove Downloads" button to delete all the locally cached data. + +## How it works + +1. An `AGSPortalItem` and `AGSOfflineMapTask` are used to load the popup button with the available `AGSPreplannedMapArea` objects. +2. On selection, the data for the `AGSPreplannedMapArea` is downloaded using `downloadPreplannedOfflineMapJob(with preplannedMapArea: AGSPreplannedMapArea, downloadDirectory: URL) ` on the `AGSOfflineMapTask ` object. +3. On subsequent selections, the downloaded map is reloaded using `AGSMobileMapPackage(fileURL: URL)`. + +## Relevant API + +* `AGSOfflineMapTask` +* `AGSPreplannedMapArea` +* `AGSDownloadPreplannedOfflineMapJob` +* `AGSDownloadPreplannedOfflineMapResult` +* `AGSMobileMapPackage` \ No newline at end of file diff --git a/arcgis-runtime-samples-macos/Maps/Download preplanned map/image1.png b/arcgis-runtime-samples-macos/Maps/Download preplanned map/image1.png new file mode 100644 index 0000000..32005cd Binary files /dev/null and b/arcgis-runtime-samples-macos/Maps/Download preplanned map/image1.png differ