-
Notifications
You must be signed in to change notification settings - Fork 12
[New] Sample: Download raster tiles to local cache #792
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chriswebb09
wants to merge
28
commits into
v.next
Choose a base branch
from
chrisw/download-raster-tile
base: v.next
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 13 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
34577d4
initial commit for sample
chriswebb09 9e082f4
update for code error
chriswebb09 b7966ea
update screenshot and envelope logic
chriswebb09 09e5be4
update readme
chriswebb09 01f5610
Apply suggestions from code review
chriswebb09 913310a
update readme
chriswebb09 907e213
remove trailing space
chriswebb09 cb6ef2a
fix max scale
chriswebb09 e431bb7
cleanup code
chriswebb09 a52c300
Apply suggestions from code review
chriswebb09 e34a2e9
add fallback
chriswebb09 f20b130
Apply suggestions from code review
chriswebb09 8f09cda
Apply suggestions from code review
chriswebb09 dfcb275
update url pattrn
chriswebb09 2074d04
add drag gesture
chriswebb09 0ad9a13
Apply suggestions from code review
chriswebb09 8de4ae1
Apply suggestions from code review
chriswebb09 f74da13
cleanup extra space
chriswebb09 c2aaf22
add in change for rectangle
chriswebb09 59124db
change error to struct
chriswebb09 64d2aaa
update screenshot
chriswebb09 8082850
remove viewbuilder
chriswebb09 2bbeb37
Apply suggestions from code review
chriswebb09 6ee7764
Apply suggestions from code review
chriswebb09 ff0f866
add in docstring
chriswebb09 a221c8a
Merge remote-tracking branch 'refs/remotes/origin/chrisw/download-ras…
chriswebb09 8935971
add in review suggestions
chriswebb09 cadfb54
remove extra space
chriswebb09 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
297 changes: 297 additions & 0 deletions
297
...ed/Samples/Download raster tiles to local cache/DownloadRasterTilesToLocalCacheView.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,297 @@ | ||
| // Copyright 2026 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 | ||
| // | ||
| // https://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 ArcGIS | ||
| import SwiftUI | ||
|
|
||
| struct DownloadRasterTilesToLocalCacheView: View { | ||
| /// The view model for the sample. | ||
| @State private var model = Model() | ||
|
|
||
| /// A Boolean value indicating whether the exported tiles preview is showing. | ||
| @State private var isShowingPreview = false | ||
|
|
||
| /// The error shown in the error alert. | ||
| @State private var error: (any Error)? | ||
|
|
||
| var body: some View { | ||
| GeometryReader { geometryProxy in | ||
| MapViewReader { mapViewProxy in | ||
| MapView(map: model.map) | ||
| .interactionModes(model.exportTileCacheJob == nil ? [.pan, .zoom] : []) | ||
| .onScaleChanged { model.mapViewScale = $0 } | ||
| .onDisappear { | ||
| Task { await model.cancelExport() } | ||
| } | ||
| .overlay { | ||
| // Draws a red rectangle to emphasize the extent that | ||
| // will be exported. | ||
| let rect = exportExtentRect(in: geometryProxy.size) | ||
| Rectangle() | ||
| .stroke(.red, lineWidth: 2) | ||
| .frame(width: rect.width, height: rect.height) | ||
| .position(x: rect.midX, y: rect.midY) | ||
| } | ||
| .overlay(alignment: .center) { | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| if let job = model.exportTileCacheJob { | ||
| exportProgressView(job: job) | ||
| } | ||
| } | ||
| .toolbar { | ||
| ToolbarItem(placement: .bottomBar) { | ||
| Button("Export Tiles") { | ||
| Task { | ||
| await exportTiles( | ||
| mapViewProxy: mapViewProxy, | ||
| size: geometryProxy.size | ||
| ) | ||
| } | ||
| } | ||
| .disabled(model.exportTileCacheJob != nil) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| .sheet(isPresented: $isShowingPreview, onDismiss: model.removePreview) { | ||
| previewSheet | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| } | ||
| .errorAlert(presentingError: $error) | ||
| } | ||
|
|
||
| /// A progress indicator and cancel button shown while tiles are exporting. | ||
| private func exportProgressView(job: ExportTileCacheJob) -> some View { | ||
| VStack(spacing: 16) { | ||
| Text("Exporting tiles…") | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| // Observes the job's progress to update the bar automatically. | ||
| ProgressView(job.progress) | ||
| .progressViewStyle(.linear) | ||
| Button("Cancel", role: .destructive) { | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| Task { await model.cancelExport() } | ||
| } | ||
| } | ||
| .padding() | ||
| .frame(maxWidth: 220) | ||
| .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /// A sheet that previews the exported tile cache in its own map. | ||
| @ViewBuilder private var previewSheet: some View { | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| NavigationStack { | ||
| if let previewMap = model.previewMap { | ||
| MapView(map: previewMap) | ||
| .navigationTitle("Exported Tiles") | ||
| .navigationBarTitleDisplayMode(.inline) | ||
| .toolbar { | ||
| ToolbarItem(placement: .confirmationAction) { | ||
| Button("Done") { isShowingPreview = false } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Exports the tiles within the red rectangle and shows the preview sheet. | ||
| /// - Parameters: | ||
| /// - mapViewProxy: The proxy used to convert screen points to locations. | ||
| /// - size: The size of the map view, used to locate the export extent. | ||
| private func exportTiles(mapViewProxy: MapViewProxy, size: CGSize) async { | ||
| // Creates an envelope from the centered square. | ||
| guard let extent = mapViewProxy.envelope(fromViewRect: exportExtentRect(in: size)) else { return } | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
|
|
||
| do { | ||
| try await model.exportTiles(extent: extent) | ||
| isShowingPreview = true | ||
| } catch { | ||
| self.error = error | ||
| } | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /// Returns a square export extent centered in the map view. | ||
| private func exportExtentRect(in size: CGSize) -> CGRect { | ||
| let sideLength = min(size.width, size.height) | ||
| return CGRect( | ||
| x: (size.width - sideLength) / 2, | ||
| y: (size.height - sideLength) / 2, | ||
| width: sideLength, | ||
| height: sideLength | ||
| ) | ||
| } | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| extension DownloadRasterTilesToLocalCacheView { | ||
| /// The model that stores the maps and runs the export task for this sample. | ||
| @MainActor | ||
| @Observable | ||
| final class Model { | ||
| /// A map with the World Ocean Base tiled layer as its basemap. | ||
| let map: Map | ||
|
|
||
| /// A map that previews the exported tile cache, if one exists. | ||
| private(set) var previewMap: Map? | ||
|
|
||
| /// The export job currently downloading the tile package, if any. | ||
| private(set) var exportTileCacheJob: ExportTileCacheJob? | ||
|
|
||
| /// The tiled layer that provides both the basemap and the export source. | ||
| private let tiledLayer = ArcGISTiledLayer(url: .worldOceanBase) | ||
|
|
||
| /// The task that exports tiles from the tiled layer's service. | ||
| private let exportTask = ExportTileCacheTask(url: .worldOceanBase) | ||
|
|
||
| /// A URL to the temporary directory storing the exported tile package. | ||
| private let temporaryDirectory = FileManager.createTemporaryDirectory() | ||
|
|
||
| /// The current scale of the map view, used to derive the export scale range. | ||
| var mapViewScale = 0.0 | ||
|
|
||
| init() { | ||
| // Creates a map with a basemap made from the tiled layer, and limits | ||
| // its minimum scale to avoid requesting a huge download. | ||
| map = Map(basemap: Basemap(baseLayer: tiledLayer)) | ||
| map.minScale = 1e7 | ||
| map.initialViewpoint = Viewpoint( | ||
| center: Point(latitude: 34, longitude: -117), | ||
| scale: 1e7 | ||
| ) | ||
| } | ||
|
|
||
| deinit { | ||
| // Removes the temporary directory and all of its content. | ||
| try? FileManager.default.removeItem(at: temporaryDirectory) | ||
| } | ||
|
|
||
| /// Exports the tiles within an extent to a local tile package and builds | ||
| /// a preview map from the result. | ||
| /// - Parameter extent: The geographic area of interest to export. | ||
| func exportTiles(extent: Envelope) async throws { | ||
| // Loads the task to access its service metadata. | ||
| try await exportTask.load() | ||
| guard let mapServiceInfo = exportTask.mapServiceInfo, | ||
| mapServiceInfo.allowsExportTiles else { | ||
| throw ExportError.notSupported | ||
| } | ||
|
|
||
| // Creates the parameters for the export tile cache job. | ||
| let parameters = try await makeExportTileCacheParameters(areaOfInterest: extent) | ||
|
|
||
| // Uses the compact V2 format (.tpkx) when supported, otherwise the | ||
| // legacy compact format (.tpk). | ||
| let fileExtension = mapServiceInfo.allowsExportTileCacheCompactV2 ? "tpkx" : "tpk" | ||
| let downloadURL = temporaryDirectory | ||
| .appendingPathComponent( | ||
| "myTileCache", | ||
| isDirectory: false | ||
| ) | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| .appendingPathExtension(fileExtension) | ||
| try? FileManager.default.removeItem(at: downloadURL) | ||
|
|
||
| // Creates the export job based on the parameters and temporary URL. | ||
| exportTileCacheJob = exportTask.makeExportTileCacheJob( | ||
| parameters: parameters, | ||
| downloadFileURL: downloadURL | ||
| ) | ||
| defer { | ||
| exportTileCacheJob = nil | ||
| } | ||
| guard let exportTileCacheJob else { return } | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Starts the job. | ||
| exportTileCacheJob.start() | ||
|
|
||
| // Awaits the resulting tile cache and builds a preview map from it. | ||
| let tileCache = try await exportTileCacheJob.output | ||
| let previewLayer = ArcGISTiledLayer(tileCache: tileCache) | ||
| let previewMap = Map(basemap: Basemap(baseLayer: previewLayer)) | ||
| previewMap.initialViewpoint = Viewpoint(boundingGeometry: extent) | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| self.previewMap = previewMap | ||
| } | ||
|
|
||
| /// Creates the export tile cache parameters. | ||
| /// - Parameter areaOfInterest: The area of interest to create the parameters for. | ||
| /// - Returns: An `ExportTileCacheParameters` if there are no errors. | ||
| private func makeExportTileCacheParameters(areaOfInterest: Envelope) async throws -> ExportTileCacheParameters { | ||
| // Uses the current map view scale when available; otherwise falls | ||
| // back to the map's configured minScale. | ||
| let effectiveScale = mapViewScale > 0 ? mapViewScale : map.minScale | ||
| let maxScale = (effectiveScale ?? 0) / 2 | ||
| let minScale = (effectiveScale ?? 0) * 2 | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Returns the default parameters for the export tile cache task. | ||
| return try await exportTask.makeDefaultExportTileCacheParameters( | ||
| areaOfInterest: areaOfInterest, | ||
| minScale: minScale, | ||
| maxScale: maxScale | ||
| ) | ||
| } | ||
|
|
||
| /// Cancels the running export job, if one exists. | ||
| func cancelExport() async { | ||
| if let job = exportTileCacheJob.take() { | ||
| await job.cancel() | ||
| } | ||
| } | ||
|
|
||
| /// Releases the preview map and removes the exported tile package. | ||
| func removePreview() { | ||
| previewMap = nil | ||
| let contents = (try? FileManager.default.contentsOfDirectory( | ||
| at: temporaryDirectory, | ||
| includingPropertiesForKeys: nil | ||
| )) ?? [] | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| for url in contents { | ||
| try? FileManager.default.removeItem(at: url) | ||
| } | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| private extension DownloadRasterTilesToLocalCacheView.Model { | ||
| /// An error indicating the service does not support exporting tiles. | ||
| enum ExportError: LocalizedError { | ||
| case notSupported | ||
|
|
||
| var errorDescription: String? { | ||
| switch self { | ||
| case .notSupported: | ||
| return "Exporting tiles is not supported for the service." | ||
| } | ||
| } | ||
| } | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| private extension FileManager { | ||
| /// Creates a temporary directory. | ||
| /// - Returns: The URL of the created directory. | ||
| static func createTemporaryDirectory() -> URL { | ||
| try! FileManager.default.url( | ||
| for: .itemReplacementDirectory, | ||
| in: .userDomainMask, | ||
| appropriateFor: FileManager.default.temporaryDirectory, | ||
| create: true | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private extension URL { | ||
| /// The URL of the World Ocean Base (for Export) tile service. | ||
| static var worldOceanBase: URL { | ||
| URL(string: "https://tiledbasemaps.arcgis.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer")! | ||
| } | ||
| } | ||
|
|
||
| #Preview { | ||
| NavigationStack { | ||
| DownloadRasterTilesToLocalCacheView() | ||
| } | ||
| } | ||
42 changes: 42 additions & 0 deletions
42
Shared/Samples/Download raster tiles to local cache/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # Download raster tiles to local cache | ||
|
|
||
| Download tiles to a local tile cache file stored on the device. | ||
|
|
||
|  | ||
|
|
||
| ## Use case | ||
|
|
||
| Field workers with limited network connectivity can use exported tiles as a basemap for use offline. | ||
|
|
||
| ## How to use the sample | ||
|
|
||
| Pan and zoom into the desired area, making sure the area is within the red boundary. Tap the "Export Tiles" button to start the process. On successful completion, you will see a preview of the downloaded tile package. | ||
|
|
||
| ## How it works | ||
|
|
||
| 1. Create a map and set its `minScale` to 10,000,000. Limiting the scale in this sample limits the potential size of the selection area, thereby keeping the exported tile package to a reasonable size. | ||
| 2. Create an `ExportTileCacheTask`, passing in the URL of the tiled layer. | ||
| 3. Create default `ExportTileCacheParameters` for the task, specifying extent, minimum scale and maximum scale. | ||
| 4. Use the parameters and a path to create an `ExportTileCacheJob` from the task. | ||
| 5. Start the job, and when it completes successfully, get the resulting `TileCache`. | ||
| 6. Use the tile cache to create an `ArcGISTiledLayer`, and display it in the map. | ||
|
|
||
| ## Relevant API | ||
|
|
||
| * ArcGISTiledLayer | ||
| * ExportTileCacheJob | ||
| * ExportTileCacheParameters | ||
| * ExportTileCacheTask | ||
| * TileCache | ||
|
|
||
| ## About the data | ||
|
|
||
| The sample uses a [World Ocean Base (for Export)](https://www.arcgis.com/home/item.html?id=5d85d897aee241f884158aa514954443) map service that features marine bathymetry. The service supports "estimate export tiles size" and "export tiles" operations. | ||
|
|
||
| ## Additional information | ||
|
|
||
| ArcGIS tiled layers do not support reprojection, query, select, identify, or editing. See the [Layer types](https://developers.arcgis.com/swift/layers/) discussion in the developers guide to learn more about the characteristics of ArcGIS tiled layers. The map service behind a tiled layer may support [Export Tiles](https://developers.arcgis.com/rest/services-reference/enterprise/export-tiles-map-service/) operation. You can also specify the maximum tiles clients will be allowed to download for the service. | ||
|
|
||
| ## Tags | ||
|
|
||
| cache, download, offline |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.