Animate images with image overlay

View on GitHub
Sample viewer app

Animate a series of images with an image overlay.

Image of animate images with image overlay

Use case

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

  1. Create an AGSImageOverlay and add it to the AGSSceneView.
  2. Set up a repeating timer with an initial interval time of 16ms, which will display approximately 60 AGSImageFrames per second.
  3. 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
                                                                                                                                                                                                                     
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
211
212
213
// 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

class AnimateImagesWithImageOverlayViewController: UIViewController {
    // MARK: Storyboard views

    /// The button to play image overlay animation.
    @IBOutlet var playButtonItem: UIBarButtonItem!
    /// The button to pause image overlay animation.
    @IBOutlet var pauseButtonItem: UIBarButtonItem!
    /// The button to choose a playback speed for the animation.
    @IBOutlet weak var speedButtonItem: UIBarButtonItem!
    /// The toolbar in the view controller.
    @IBOutlet weak var toolbar: UIToolbar!
    /// The label to display opacity level.
    @IBOutlet weak var opacityLabel: UILabel!

    /// The slider to change opacity level, from transparent 0% to opaque 100%.
    @IBOutlet weak var opacitySlider: UISlider! {
        didSet {
            sliderValueChanged(opacitySlider)
        }
    }

    /// The scene view managed by the view controller.
    @IBOutlet weak var 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.
    private lazy var 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)! }
        return CircularIterator(elements: images)
    }()

    /// A formatter to format percentage strings.
    let percentageFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .percent
        formatter.multiplier = 100
        return 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.
    func makeScene() -> AGSScene {
        // Create a tiled layer from World Dark Gray Base REST service.
        let basemapTileURL = URL(string: "https://services.arcgisonline.com/arcgis/rest/services/Canvas/World_Dark_Gray_Base/MapServer")!
        let worldDarkGrayBasemap = AGSArcGISTiledLayer(url: basemapTileURL)
        // 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(basemap: AGSBasemap(baseLayer: worldDarkGrayBasemap))
        scene.baseSurface = surface
        return scene
    }

    /// Create a display link timer for the image overlay animation.
    ///
    /// - Returns: A new `CADisplayLink` object.
    func makeDisplayLink() -> 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.
    @objc
    func setImageFrame() {
        let frame = AGSImageFrame(image: imagesIterator.next()!, extent: pacificSouthwestEnvelope)
        imageOverlay.imageFrame = frame
    }

    // MARK: - Actions

    @IBAction func sliderValueChanged(_ slider: UISlider) {
        imageOverlay.opacity = slider.value
        opacityLabel.text = percentageFormatter.string(from: slider.value as NSNumber)!
    }

    @IBAction func playPauseButtonTapped(_ button: UIBarButtonItem) {
        let index = toolbar.items!.firstIndex(of: button)!
        toolbar.items![index] = displayLink.isPaused ? pauseButtonItem : playButtonItem
        displayLink.isPaused.toggle()
    }

    @IBAction func speedButtonTapped(_ 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) in
            let action = UIAlertAction(title: name, style: .default) { _ in
                self.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: UIViewController

    override func viewDidLoad() {
        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.")
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        displayLink = makeDisplayLink()
    }

    override func viewDidDisappear(_ 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.
private struct CircularIterator<Element>: IteratorProtocol {
    let elements: [Element]
    private var elementIterator: Array<Element>.Iterator

    init(elements: [Element]) {
        self.elements = elements
        elementIterator = elements.makeIterator()
    }

    mutating func next() -> Element? {
        if let next = elementIterator.next() {
            return next
        } else {
            elementIterator = elements.makeIterator()
            return elementIterator.next()
        }
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.