Line of sight (geoelement)

View on GitHub
Sample viewer app

Show a line of sight between two moving objects.

Line of sight (geoelement)

Use case

A line of sight between AGSGeoElements (i.e. observer and target) will not remain constant whilst one or both are on the move.

An AGSGeoElementLineOfSight is therefore useful in cases where visibility between two AGSGeoElements requires monitoring over a period of time in a partially obstructed field of view (such as buildings in a city).

How to use the sample

A line of sight will display between a point on the Empire State Building (observer) and a taxi (target). The taxi will drive around a block and the line of sight should automatically update. The taxi will be highlighted when it is visible. You can change the observer height with the slider to see how it affects the target's visibility.

How it works

  1. Instantiate an AGSAnalysisOverlay and add it to the AGSSceneView's analysis overlays collection.
  2. Instantiate an AGSGeoElementLineOfSight, passing in an AGSGeoElement (feature or graphic) for both the observer and the target. Add the line of sight to the analysis overlay's analyses array.
  3. To get the target visibility when it changes, observe the target visibility changing on the AGSGeoElementLineOfSight instance.

Relevant API

  • AGSAnalysisOverlay
  • AGSGeoElementLineOfSight
  • AGSLineOfSight.targetVisibility

Offline data

This sample uses the Taxi CAD Drawing. It is downloaded from ArcGIS Online automatically.

Tags

3D, line of sight, visibility, visibility analysis

Sample Code

LineOfSightGeoElementViewController.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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// 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 UIKit
import ArcGIS

class LineOfSightGeoElementViewController: UIViewController {
    @IBOutlet weak var sceneView: AGSSceneView!

    @IBOutlet weak var targetVisibilityLabel: UILabel!
    @IBOutlet weak var observerZLabel: UILabel!

    @IBOutlet weak var observerZSlider: UISlider!
    @IBOutlet weak var observerZMinLabel: UILabel!
    @IBOutlet weak var observerZMaxLabel: UILabel!

    // properties for setting up and manipulating the scene
    private let scene: AGSScene
    private let overlay: AGSGraphicsOverlay
    private let taxiGraphic: AGSGraphic
    private let observerGraphic: AGSGraphic
    private let lineOfSight: AGSGeoElementLineOfSight

    private let observerZMin = 20.0
    private let observerZMax = 1500.0
    /// The roof height in meters of Empire State Building.
    private let observerZ = 380.0

    private let observerPoint: AGSPoint

    // locations used in the sample
    private let streetIntersectionLocations = [
        AGSPoint(x: -73.985068, y: 40.747786, spatialReference: .wgs84()),
        AGSPoint(x: -73.983452, y: 40.747091, spatialReference: .wgs84()),
        AGSPoint(x: -73.982961, y: 40.747762, spatialReference: .wgs84()),
        AGSPoint(x: -73.984513, y: 40.748469, spatialReference: .wgs84())
    ]

    // handle onto any line of sight KVO observer
    private var losObserver: NSKeyValueObservation?

    private var initialViewpointCenter: AGSPoint {
        // If possible, find the middle of the block that the taxi will drive around, or else focus on the observer
        return AGSGeometryEngine.unionGeometries(streetIntersectionLocations)?.extent.center ?? observerPoint
    }

    required init?(coder aDecoder: NSCoder) {
        // ====================================
        // set up the scene, layers and overlay
        // ====================================

        observerPoint = AGSPoint(x: -73.984988, y: 40.748131, z: observerZ, spatialReference: .wgs84())

        // initialize the scene with an imagery basemap
        scene = AGSScene(basemap: .imageryWithLabels())

        /// The url of the Terrain 3D ArcGIS REST Service.
        let worldElevationServiceURL = URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!
        // initialize the elevation source and add it to the base surface of the scene
        let elevationSrc = AGSArcGISTiledElevationSource(url: worldElevationServiceURL)
        scene.baseSurface?.elevationSources.append(elevationSrc)

        /// The url of a scene service for buildings in New York, U.S.
        let newYorkBuildingsServiceURL = URL(string: "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/New_York_LoD2_3D_Buildings/SceneServer/layers/0")!
        // add some buildings to the scene
        let sceneLayer = AGSArcGISSceneLayer(url: newYorkBuildingsServiceURL)
        scene.operationalLayers.add(sceneLayer)

        // initialize a graphics overlay
        overlay = AGSGraphicsOverlay()
        overlay.sceneProperties = AGSLayerSceneProperties(surfacePlacement: .relative)

        // =====================================================
        // initialize two graphics for both display and analysis
        // =====================================================

        // initialize the taxi graphic
        let taxiSymbol = AGSModelSceneSymbol(name: "dolmus", extension: "3ds", scale: 1)
        taxiSymbol.anchorPosition = .bottom
        taxiGraphic = AGSGraphic(geometry: streetIntersectionLocations[streetIntersectionLocations.startIndex], symbol: taxiSymbol, attributes: nil)

        // initialize the observer graphic
        let observerSymbol = AGSSimpleMarkerSceneSymbol(style: .sphere, color: .red, height: 10, width: 10, depth: 10, anchorPosition: .center)
        observerGraphic = AGSGraphic(geometry: observerPoint, symbol: observerSymbol, attributes: nil)

        // ================
        // use the graphics
        // ================

        // add the taxi and observer to the graphics overlay
        overlay.graphics.addObjects(from: [observerGraphic, taxiGraphic])

        // initialize the line of sight analysis between the observer and taxi
        lineOfSight = AGSGeoElementLineOfSight(observerGeoElement: observerGraphic, targetGeoElement: taxiGraphic)

        super.init(coder: aDecoder)

        // set the initial viewpoint
        scene.initialViewpoint = AGSViewpoint(center: initialViewpointCenter, scale: 6000)

        // default to a line of sight target offset of 1.5m above ground
        lineOfSight.targetOffsetZ = 1.5

        // let's examine the 3D model symbol to see if we can determine a better height
        taxiSymbol.load { [weak self] error in
            guard error == nil else {
                print("Error loading the taxi symbol: \(error!.localizedDescription)")
                return
            }
            // use the model's height as the line of sight target offset above ground
            self?.lineOfSight.targetOffsetZ = taxiSymbol.height
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // add the source code button item to the right of navigation bar
        (navigationItem.rightBarButtonItem as! SourceCodeBarButtonItem).filenames = ["LineOfSightGeoElementViewController"]

        // assign the scene to the scene view
        sceneView.scene = scene

        // Add a graphics overlay for the taxi and observer
        sceneView.graphicsOverlays.add(overlay)

        // create an analysis overlay using a single Line of Sight and add it to the scene view
        let analysisOverlay = AGSAnalysisOverlay()
        analysisOverlay.analyses.add(lineOfSight)
        sceneView.analysisOverlays.add(analysisOverlay)

        // update the UI if the Line of Sight analysis result changes
        losObserver = lineOfSight.observe(\.targetVisibility, options: .new) { [weak self] (losAnalysis, _) in
            DispatchQueue.main.async {
                self?.updateLineOfSightVisibilityLabel(visibility: losAnalysis.targetVisibility)
            }
        }

        // initialize the observer z slider
        observerZSlider.minimumValue = Float(observerZMin)
        observerZSlider.maximumValue = Float(observerZMax)

        observerZMinLabel.text = getFormattedString(z: observerZMin)
        observerZMaxLabel.text = getFormattedString(z: observerZMax)
    }

    // update the observer height when the slider is moved
    @IBAction func observerHeightChanged(_ observerZSlider: UISlider) {
        if let oldLocation = observerGraphic.geometry as? AGSPoint,
            let newLocation = AGSGeometryEngine.geometry(bySettingZ: Double(observerZSlider.value), in: oldLocation) as? AGSPoint {
            observerGraphic.geometry = newLocation
            updateObserverZLabel()
        }
    }

    // Clean up when done with the sample
    deinit {
        losObserver?.invalidate()
    }

    // start and stop animation
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        startAnimation()

        // set the line width (default 1.0). This setting is applied to all line of sight analysis in the view
        AGSLineOfSight.setLineWidth(2.0)

        // update the ui
        observerZSlider.value = Float(observerPoint.z)
        updateObserverZLabel()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        animationTimer?.invalidate()
    }

    // current line of sight status
    private func updateLineOfSightVisibilityLabel(visibility: AGSLineOfSightTargetVisibility) {
        switch visibility {
        case .obstructed:
            targetVisibilityLabel.text = "Obstructed"
            taxiGraphic.isSelected = false
        case .visible:
            targetVisibilityLabel.text = "Visible"
            taxiGraphic.isSelected = true
        case .unknown:
            fallthrough
        @unknown default:
            targetVisibilityLabel.text = "Unknown"
            taxiGraphic.isSelected = false
        }
    }

    private func updateObserverZLabel() {
        observerZLabel.text = {
            guard let observerLocation = observerGraphic.geometry as? AGSPoint, observerLocation.hasZ else {
                return "Unknown"
            }
            return getFormattedString(z: observerLocation.z)
        }()
    }

    // Track animation progress
    private var animationProgess = (frameIndex: 0, pointIndex: 0)
    private var animationTimer: Timer?
    private let framesPerSegment = 150

    private func startAnimation() {
        // Kick off a timer
        animationProgess = (frameIndex: 0, pointIndex: streetIntersectionLocations.startIndex)
        animationTimer = Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { [weak self] _ in
            self?.performAnimationFrame()
        }
    }

    private func performAnimationFrame() {
        animationProgess.frameIndex += 1

        // See if we've reached the next intersection.
        if animationProgess.frameIndex == self.framesPerSegment {
            // Move to the next segment
            animationProgess.frameIndex = 0
            animationProgess.pointIndex += 1
            // And if we've visited all intersections, start at the beginning again
            if animationProgess.pointIndex == streetIntersectionLocations.endIndex {
                animationProgess.pointIndex = streetIntersectionLocations.startIndex
            }
        }

        // Get the current taxi position between two intersections as well as its heading
        let startPoint = streetIntersectionLocations[animationProgess.pointIndex]
        let endPoint = streetIntersectionLocations[(animationProgess.pointIndex + 1) % streetIntersectionLocations.endIndex]
        let progress = Double(animationProgess.frameIndex) / Double(framesPerSegment)
        let (animationPoint, heading) = interpolatedPoint(firstPoint: startPoint, secondPoint: endPoint, progress: progress)

        // Update the taxi graphic's potision and heading
        taxiGraphic.geometry = animationPoint
        (taxiGraphic.symbol as? AGSModelSceneSymbol)?.heading = heading
    }

    // Formatting z values for locale
    private let zValuesFormatter: MeasurementFormatter = {
        let formatter = MeasurementFormatter()
        formatter.numberFormatter.maximumFractionDigits = 0
        formatter.numberFormatter.roundingMode = .down
        formatter.unitOptions = .providedUnit
        return formatter
    }()

    private func getFormattedString(z value: Double) -> String {
        return zValuesFormatter.string(from: Measurement<UnitLength>(value: value, unit: .meters))
    }
}

private func interpolatedPoint(firstPoint: AGSPoint, secondPoint: AGSPoint, progress: Double) -> (AGSPoint, Double) {
    // Use the geometry engine to calculate the heading between point 1 and 2
    let geResult = AGSGeometryEngine.geodeticDistanceBetweenPoint1(
        firstPoint,
        point2: secondPoint,
        distanceUnit: .meters(),
        azimuthUnit: .degrees(),
        curveType: .geodesic)
    let heading = geResult?.azimuth1 ?? 0

    // calculate the point representing progress towards the next point (cartesian calculation works fine at this scale)
    let diff = (x: (secondPoint.x - firstPoint.x) * progress,
                y: (secondPoint.y - firstPoint.y) * progress,
                z: (secondPoint.z - firstPoint.z) * progress)

    return (AGSPoint(x: firstPoint.x + diff.x,
                     y: firstPoint.y + diff.y,
                     z: firstPoint.z + diff.z,
                     spatialReference: firstPoint.spatialReference), heading)
}

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