Skip to content

Show line of sight between geoelements

View on GitHub

Show a line of sight between two moving objects.

Image of Show line of sight between geoelements sample

Use case

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

A GeoElementLineOfSight is therefore useful in cases where visibility between two GeoElements 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 AnalysisOverlay and add it to the SceneView's analysis overlays collection.
  2. Instantiate a GeoElementLineOfSight, passing in observer and target GeoElements (features or graphics). Add the line of sight to the analysis overlay's analyses collection.
  3. To get the target visibility when it changes, react to the target visibility changing on the GeoElementLineOfSight instance.

Relevant API

  • AnalysisOverlay
  • GeoElementLineOfSight
  • LineOfSight.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

ShowLineOfSightBetweenGeoelementsView.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
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// Copyright 2025 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 ShowLineOfSightBetweenGeoelementsView: View {
    /// The view model for the sample.
    @State private var model = Model()

    /// A Boolean value indicating whether the settings sheet is presented.
    @State private var isPresented = false

    var body: some View {
        SceneView(
            scene: model.scene,
            graphicsOverlays: [model.graphicsOverlay],
            analysisOverlays: [model.analysisOverlay]
        )
        .onAppear {
            model.setupAnimation()
        }
        .onDisappear {
            model.stopAnimating()
        }
        .overlay(alignment: .top) {
            HStack {
                Text("Visibility:")
                Text(model.targetVisibility.label)
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 6)
            .background(.thinMaterial, ignoresSafeAreaEdges: .horizontal)
        }
        .toolbar {
            ToolbarItemGroup(placement: .bottomBar) {
                Button("Settings") {
                    isPresented = true
                }
                .popover(isPresented: $isPresented) {
                    settingsSheet
                }
            }
        }
    }

    /// The settings configuration sheet for adjusting the observer's height.
    private var settingsSheet: some View {
        NavigationStack {
            Form {
                let heightRange = 20.0...70.0
                LabeledContent(
                    "Observer Height",
                    value: Measurement(value: model.height, unit: UnitLength.meters),
                    format: .measurement(width: .abbreviated)
                )
                Slider(
                    value: $model.height,
                    in: heightRange,
                    step: 1
                ) {
                    Text("Observer Height")
                } minimumValueLabel: {
                    Text(
                        Measurement(
                            value: heightRange.lowerBound,
                            unit: UnitLength.meters
                        ),
                        format: .measurement(width: .abbreviated)
                    )
                } maximumValueLabel: {
                    Text(
                        Measurement(
                            value: heightRange.upperBound,
                            unit: UnitLength.meters
                        ),
                        format: .measurement(width: .abbreviated)
                    )
                }
                .listRowSeparator(.hidden, edges: .top)
            }
            .presentationDetents([.fraction(0.25)])
            .navigationTitle("Settings")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("Done") {
                        isPresented = false
                    }
                }
            }
        }
        .frame(idealWidth: 320, idealHeight: 380)
    }
}

private extension ShowLineOfSightBetweenGeoelementsView {
    /// View model responsible for setting up the 3D scene, graphics, animation,
    /// and performing line of sight analysis between moving and static geoelements.
    @MainActor
    @Observable
    final class Model {
        /// A set of predefined waypoints for animating the taxi's movement.
        private let points = [
            Point(latitude: 40.748469, longitude: -73.984513),
            Point(latitude: 40.747786, longitude: -73.985068),
            Point(latitude: 40.747091, longitude: -73.983452),
            Point(latitude: 40.747762, longitude: -73.982961)
        ]

        /// The height of the observer in meters. Updates the observer graphic when changed.
        var height = 20.0 {
            didSet {
                /// Updates the height value of the observer's point geometry.
                guard let geometry = observerGraphic.geometry as? Point else { return }
                observerGraphic.geometry = GeometryEngine.makeGeometry(from: geometry, z: height)
            }
        }

        /// The current frame number in the animation sequence for the taxi's movement.
        @ObservationIgnored private var frameIndex = 0

        /// The total number of animation frames to complete a segment between two waypoints.
        private let frameMax = 120

        /// The index of the current start point in the `points` array used for animating the taxi's path.
        @ObservationIgnored private var pointIndex = 0

        /// The 3D scene containing basemap, elevation, and building layers.
        let scene: ArcGIS.Scene = {
            // Create a scene and set an initial viewpoint.
            let scene = Scene(basemapStyle: .arcGISImagery)
            // Add base surface from elevation service.
            let elevationSource = ArcGISTiledElevationSource(url: .elevationService)
            scene.baseSurface.addElevationSource(elevationSource)
            let buildingLayer = ArcGISSceneLayer(url: .buildingsService)
            scene.addOperationalLayer(buildingLayer)
            let camera = Camera(
                lookingAt: .observerPoint,
                distance: 700.0,
                heading: -30.0,
                pitch: 45.0,
                roll: 0.0
            )
            scene.initialViewpoint = Viewpoint(
                boundingGeometry: .observerPoint,
                camera: camera
            )
            return scene
        }()

        /// The graphics overlay used to render the observer and target symbols.
        let graphicsOverlay: GraphicsOverlay = {
            let overlay = GraphicsOverlay()
            overlay.sceneProperties.surfacePlacement = .relative
            return overlay
        }()

        /// An analysis overlay used to display the line of sight analysis visualization.
        let analysisOverlay = AnalysisOverlay()

        /// A line of sight analysis between the observer and the taxi graphic.
        private let lineOfSight: GeoElementLineOfSight

        /// A graphic representing the taxi model that will be animated.
        private let taxiGraphic: Graphic = {
            let sceneSymbol = ModelSceneSymbol(url: .taxi)
            sceneSymbol.anchorPosition = .bottom
            let graphic = Graphic(
                geometry: .taxiPoint,
                symbol: sceneSymbol
            )
            return graphic
        }()

        /// A graphic representing the observer's location in the scene.
        private let observerGraphic = Graphic(
            geometry: .observerPoint,
            symbol: SimpleMarkerSceneSymbol(
                style: .sphere,
                color: .red,
                height: 5,
                width: 5,
                depth: 5,
                anchorPosition: .bottom
            )
        )

        /// A timer to synchronize the taxi animation to the refresh rate of the display.
        @ObservationIgnored private var displayLink: CADisplayLink!

        /// The target visibility of the taxi graphic from the point of view of the observer.
        var targetVisibility: GeoElementLineOfSight.TargetVisibility = .unknown

        init() {
            graphicsOverlay.addGraphics([observerGraphic, taxiGraphic])
            lineOfSight = GeoElementLineOfSight(observer: observerGraphic, target: taxiGraphic)
            lineOfSight.targetOffsetZ = 2
            analysisOverlay.addAnalysis(lineOfSight)
        }

        /// Sets up and starts the animation for the taxi graphic. It initializes the `displayLink` property
        /// that allows the sample to synchronize its drawing to the refresh rate of the display. It calls the `animateTaxiGraphic()`
        /// method on every screen refresh ensuring smooth animation of the taxi as it moves along the waypoints.
        func setupAnimation() {
            displayLink = makeDisplayLink()
            displayLink.isPaused = false
        }

        /// Creates a display link timer for the image overlay animation.
        /// - Returns: A new `CADisplayLink` object.
        private func makeDisplayLink() -> CADisplayLink {
            // Create new display link.
            let newDisplayLink = CADisplayLink(
                target: self,
                selector: #selector(animateTaxiGraphic)
            )
            // Set the default frame rate to 60 fps.
            newDisplayLink.preferredFramesPerSecond = 60
            // Add to main thread common mode run loop, so it is not effected by UI events.
            newDisplayLink.add(to: .main, forMode: .common)
            return newDisplayLink
        }

        /// Stops animating the taxi graphic.
        func stopAnimating() {
            displayLink.invalidate()
            displayLink = nil
        }

        /// Animates the target graphic between a set of points in a loop,
        /// updating the heading and visibility analysis on each frame.
        @objc
        private func animateTaxiGraphic() {
            // Increment the frame counter
            frameIndex += 1
            // Reset frame counter when segment is completed
            if frameIndex == frameMax {
                frameIndex = 0
                pointIndex += 1
                if pointIndex == points.count {
                    pointIndex = 0
                }
            }
            let starting = points[pointIndex]
            let ending = points[(pointIndex + 1) % points.count]
            let progress = Double(frameIndex) / Double(frameMax)
            // Interpolate between points.
            let intermediatePoint = interpolatedPoint(
                from: starting,
                to: ending,
                progress: progress
            )
            taxiGraphic.geometry = intermediatePoint
            if let distance = GeometryEngine.geodeticDistance(
                from: starting,
                to: ending,
                distanceUnit: .meters,
                azimuthUnit: .degrees,
                curveType: .geodesic
            ) {
                (taxiGraphic.symbol as? ModelSceneSymbol)?.heading = Float(distance.azimuth1.value)
            }
            targetVisibility = lineOfSight.targetVisibility
        }

        /// Returns a point interpolated between two coordinates based on a progress ratio.
        /// - Parameters:
        ///   - startPoint: The start point.
        ///   - endPoint: The end point.
        ///   - progress: A value representing interpolation progress.
        /// - Returns: An interpolated point based on the progress value.
        private func interpolatedPoint(from startPoint: Point, to endPoint: Point, progress: Double) -> Point {
            let x = startPoint.x + (endPoint.x - startPoint.x) * progress
            let y = startPoint.y + (endPoint.y - startPoint.y) * progress
            return Point(x: x, y: y, spatialReference: .wgs84)
        }
    }
}

private extension GeoElementLineOfSight.TargetVisibility {
    /// A human-readable label for each target visibility.
    var label: String {
        switch self {
        case .visible: "Visible"
        case .obstructed: "Obstructed"
        case .unknown: "Unknown"
        @unknown default: "Unknown"
        }
    }
}

private extension Geometry {
    /// A point representing the observer's location in New York City.
    static var observerPoint: Point {
        Point(
            latitude: 40.748131,
            longitude: -73.984988
        )
    }
    /// A point representing the initial position of the taxi in New York City.
    static var taxiPoint: Point {
        Point(
            latitude: 40.748469,
            longitude: -73.984513
        )
    }
}

extension URL {
    /// The URL of the Terrain 3D ArcGIS REST Service.
    static var elevationService: URL {
        URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!
    }

    /// The URL of a New York City buildings scene service.
    static var buildingsService: URL {
        URL(string: "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/Buildings_NewYork_v18/SceneServer/layers/0")!
    }

    /// The URL to the taxi model file.
    static var taxi: URL {
        Bundle.main.url(forResource: "dolmus", withExtension: "3ds", subdirectory: "Dolmus3ds")!
    }
}

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