An image overlay is useful for displaying fast and dynamic images; for example, rendering real-time sensor data captured from a drone. Each frame from the drone becomes a static image which is updated on the fly as the data is made available.
How to use the sample
The sample loads a map of the Southwestern United States. Tap the play or pause buttons to start or pause the radar animation. Select a playback speed to decide how quickly the animation plays. Move the slider to change the opacity of the image overlay.
How it works
Create an AGSImageOverlay and add it to the AGSSceneView.
Set up a repeating timer with an initial interval time of 16ms, which will display approximately 60 AGSImageFrames per second.
Create a new image frame when the timer fires and set it on the image overlay.
Relevant API
AGSImageFrame
AGSImageOverlay
AGSSceneView
About the data
These radar images were captured by the US National Weather Service (NWS). They highlight the Pacific Southwest sector which is made up of part the western United States and Mexico. For more information visit the National Weather Service website. The archive for radar images can be downloaded from ArcGIS Online.
Additional information
The supported image formats are GeoTIFF, TIFF, JPEG, and PNG. AGSImageOverlay does not support the rich processing and rendering capabilities of an AGSRasterLayer. Use AGSRaster and AGSRasterLayer for static image rendering, analysis, and persistence.
Tags
3D, animation, drone, dynamic, image frame, image overlay, real time, rendering
Sample Code
AnimateImagesWithImageOverlayViewController.swift
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Copyright 2020 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 UIKit
import ArcGIS
classAnimateImagesWithImageOverlayViewController: UIViewController{
// MARK: Storyboard views/// The button to play image overlay animation.@IBOutletvar playButtonItem: UIBarButtonItem!
/// The button to pause image overlay animation.@IBOutletvar pauseButtonItem: UIBarButtonItem!
/// The button to choose a playback speed for the animation.@IBOutletweakvar speedButtonItem: UIBarButtonItem!
/// The toolbar in the view controller.@IBOutletweakvar toolbar: UIToolbar!
/// The label to display opacity level.@IBOutletweakvar opacityLabel: UILabel!
/// The slider to change opacity level, from transparent 0% to opaque 100%.@IBOutletweakvar opacitySlider: UISlider! {
didSet {
sliderValueChanged(opacitySlider)
}
}
/// The scene view managed by the view controller.@IBOutletweakvar sceneView: AGSSceneView! {
didSet {
sceneView.scene = makeScene()
let point =AGSPoint(x: -116.621, y: 24.7773, z: 856977.0, spatialReference: .wgs84())
let camera =AGSCamera(location: point, heading: 353.994, pitch: 48.5495, roll: 0)
sceneView.setViewpointCamera(camera)
sceneView.imageOverlays.add(imageOverlay)
}
}
// MARK: Instance properties/// The image overlay to show image frames.let imageOverlay =AGSImageOverlay()
/// A timer to synchronize image overlay animation to the refresh rate of the display.var displayLink: CADisplayLink!
/// An iterator to hold and loop through the overlay images.privatelazyvar imagesIterator: CircularIterator<UIImage> = {
// Get the URLs to images added to the project's folder reference.let imageURLs =Bundle.main.urls(forResourcesWithExtension: "png", subdirectory: "PacificSouthWest2") ?? []
let images = imageURLs
.sorted { $0.lastPathComponent <$1.lastPathComponent }
.map { UIImage(contentsOfFile: $0.path)! }
returnCircularIterator(elements: images)
}()
/// A formatter to format percentage strings.let percentageFormatter: NumberFormatter= {
let formatter =NumberFormatter()
formatter.numberStyle = .percent
formatter.multiplier =100return formatter
}()
/// An envelope of the pacific southwest sector for displaying the image frame.let pacificSouthwestEnvelope =AGSEnvelope(
center: AGSPoint(
x: -120.0724273439448,
y: 35.131016955536694,
spatialReference: .wgs84()
),
width: 15.09589635986124,
height: -14.3770441522488 )
// MARK: Initialize scene, create animation timer and set image frame/// Create a scene.////// - Returns: A new `AGSScene` object.funcmakeScene() -> AGSScene {
// Create an elevation source from Terrain3D REST service.let elevationServiceURL =URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!let elevationSource =AGSArcGISTiledElevationSource(url: elevationServiceURL)
let surface =AGSSurface()
surface.elevationSources = [elevationSource]
let scene =AGSScene(basemapStyle: .arcGISDarkGrayBase)
scene.baseSurface = surface
return scene
}
/// Create a display link timer for the image overlay animation.////// - Returns: A new `CADisplayLink` object.funcmakeDisplayLink() -> CADisplayLink {
let newDisplayLink =CADisplayLink(target: self, selector: #selector(setImageFrame))
// Inherit the frame rate from existing display link, or set to default 60 fps. newDisplayLink.preferredFramesPerSecond = displayLink?.preferredFramesPerSecond ??60 newDisplayLink.isPaused =true// Add to main thread common mode run loop, so it is not effected by UI events. newDisplayLink.add(to: .main, forMode: .common)
return newDisplayLink
}
/// Set current image to the image overlay.@objcfuncsetImageFrame() {
let frame =AGSImageFrame(image: imagesIterator.next()!, extent: pacificSouthwestEnvelope)
imageOverlay.imageFrame = frame
}
// MARK: - Actions@IBActionfuncsliderValueChanged(_slider: UISlider) {
imageOverlay.opacity = slider.value
opacityLabel.text = percentageFormatter.string(from: slider.value asNSNumber)! }
@IBActionfuncplayPauseButtonTapped(_button: UIBarButtonItem) {
let index = toolbar.items!.firstIndex(of: button)! toolbar.items![index] = displayLink.isPaused ? pauseButtonItem : playButtonItem
displayLink.isPaused.toggle()
}
@IBActionfuncspeedButtonTapped(_button: UIBarButtonItem) {
let alertController =UIAlertController(
title: "Choose playback speed.",
message: nil,
preferredStyle: .actionSheet
)
let speedChoices: [(name: String, fps: Int)] = [
("Fast", 60),
("Medium", 30),
("Slow", 15)
]
speedChoices.forEach { (name, fps) inlet action =UIAlertAction(title: name, style: .default) { _inself.displayLink.preferredFramesPerSecond = fps
}
alertController.addAction(action)
}
let cancelAction =UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
alertController.popoverPresentationController?.barButtonItem = speedButtonItem
present(alertController, animated: true)
}
// MARK: UIViewControlleroverridefuncviewDidLoad() {
super.viewDidLoad()
// Add the source code button item to the right of navigation bar. (self.navigationItem.rightBarButtonItem as?SourceCodeBarButtonItem)?.filenames = ["AnimateImagesWithImageOverlayViewController"]
// Set UI if the load succeeds.if!imagesIterator.elements.isEmpty {
playButtonItem.isEnabled =true speedButtonItem.isEnabled =true// Load the first frame into the scene. setImageFrame()
} else {
opacitySlider.isEnabled =false presentAlert(title: "Error", message: "Fail to load images.")
}
}
overridefuncviewWillAppear(_animated: Bool) {
super.viewWillAppear(animated)
displayLink = makeDisplayLink()
}
overridefuncviewDidDisappear(_animated: Bool) {
super.viewDidDisappear(animated)
// Pause the animation and change the tool bar button.if!displayLink.isPaused {
playPauseButtonTapped(pauseButtonItem)
}
// Invalidates display link before exiting. displayLink.invalidate()
}
}
/// A generic circular iterator.privatestructCircularIterator<Element>: IteratorProtocol{
let elements: [Element]
privatevar elementIterator: Array<Element>.Iteratorinit(elements: [Element]) {
self.elements = elements
elementIterator = elements.makeIterator()
}
mutatingfuncnext() -> Element? {
iflet next = elementIterator.next() {
return next
} else {
elementIterator = elements.makeIterator()
return elementIterator.next()
}
}
}