Analyze the viewshed for an object (GeoElement) in a scene.

Use case
A viewshed analysis is a type of visual analysis you can perform on a scene. The viewshed aims to answer the question ‘What can I see from a given location?’. The output is an overlay with two different colors: one representing the visible areas (green) and the other representing the obstructed areas (red).
How to use the sample
Tap to set a destination for the vehicle (a GeoElement). The vehicle will ‘drive’ towards the tapped location. The viewshed analysis will update as the vehicle moves.
How it works
- Create and show the scene, with an elevation source and a buildings layer.
- Add a model (the
GeoElement) to represent the observer (in this case, a tank).- Use a
SimpleRendererwhich has a heading expression set in theGraphicsOverlay. This way you can relate the viewshed’s heading to theGeoElementobject’s heading.
- Use a
- Create a
GeoElementViewshedwith configuration for the viewshed analysis. - Add the viewshed to an
AnalysisOverlayand add the overlay to the scene. - Configure the SceneView
CameraControllerto orbit the vehicle.
Relevant API
- AnalysisOverlay
- GeodeticDistanceResult
- GeoElementViewshed
- ModelSceneSymbol
- OrbitGeoElementCameraController
- static GeometryEngine.geodeticDistance(from:to:distanceUnit:azimuthUnit:curveType:)
About the data
This sample shows buildings in Brest, France Scene from ArcGIS Online. The sample uses a Tank model scene symbol hosted as an item on ArcGIS Online.
Tags
3D, analysis, buildings, model, scene, viewshed, visibility analysis
Sample code
ShowViewshedFromGeoelementInSceneView.swift
// Copyright 2023 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 ArcGISimport SwiftUI
struct ShowViewshedFromGeoelementInSceneView: View { /// The view model for the sample. @StateObject private var model = Model()
/// A timer to trigger waypoint movement animation. let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View { SceneView( scene: model.scene, cameraController: model.cameraController, graphicsOverlays: [model.graphicsOverlay], analysisOverlays: [model.analysisOverlay] ) .onSingleTapGesture { _, scenePoint in // Move the tank to the scene point on a screen tap. guard let scenePoint else { return } model.waypoint = scenePoint } .onReceive(timer) { _ in // Use a timer to animate the tank moving towards the new waypoint. if model.waypoint != nil { model.animate() } } .overlay(alignment: .top) { // Instruction text. Text("Tap on the map to move the tank and update the viewshed.") .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .background(.thinMaterial, ignoresSafeAreaEdges: .horizontal) } }}
private extension ShowViewshedFromGeoelementInSceneView { /// The view model for the sample. class Model: ObservableObject { /// A scene with an imagery basemap. let scene: ArcGIS.Scene = { let scene = Scene(basemapStyle: .arcGISImagery)
// Add elevation source to the base surface of the scene with the service URL. let elevationSource = ArcGISTiledElevationSource(url: .worldElevationService) scene.baseSurface.addElevationSource(elevationSource)
// Create the building layer and add it to the scene. let buildingsLayer = ArcGISSceneLayer(url: .brestBuildingsService) scene.addOperationalLayer(buildingsLayer)
return scene }()
/// A camera controller set to follow the tank. let cameraController: CameraController
/// The graphics overlay for the tank graphic. let graphicsOverlay: GraphicsOverlay = { let graphicsOverlay = GraphicsOverlay() graphicsOverlay.sceneProperties = LayerSceneProperties(surfacePlacement: .relative)
// Set up the heading expression for the tank. let renderer3D = SimpleRenderer() let sceneProperties = RendererSceneProperties( headingExpression: "[heading]", pitchExpression: "[pitch]", rollExpression: "[roll]" ) renderer3D.sceneProperties = sceneProperties graphicsOverlay.renderer = renderer3D
return graphicsOverlay }()
/// The analysis overlay for the tank's viewshed. let analysisOverlay = AnalysisOverlay()
/// The graphic for the tank. private let tankGraphic: Graphic = { let tankSymbol = ModelSceneSymbol(url: .bradleyTank, scale: 10) tankSymbol.heading = 90 tankSymbol.anchorPosition = .bottom let tankGraphic = Graphic( geometry: Point(latitude: 48.385624, longitude: -4.506390), attributes: ["heading": 0.0], symbol: tankSymbol ) return tankGraphic }()
/// The point for the tank to move toward. var waypoint: Point?
init() { // Create camera controller. cameraController = OrbitGeoElementCameraController( target: tankGraphic, distance: 200 )
// Add tank graphic to graphics overlay. graphicsOverlay.addGraphic(tankGraphic)
// Create a viewshed to attach to the tank. let geoElementViewshed = GeoElementViewshed( geoElement: tankGraphic, horizontalAngle: 90, verticalAngle: 40, headingOffset: 0, pitchOffset: 0, minDistance: 0.1, maxDistance: 250 )
// Offset viewshed observer location to top of tank. geoElementViewshed.offsetZ = 3
// Add the viewshed to the analysisOverlay to add to the scene. analysisOverlay.addAnalysis(geoElementViewshed) }
/// Animates the tank moving from its current point to the waypoint. func animate() { // Get point from the current tank position. guard let tankLocation = tankGraphic.geometry as? Point, let point = waypoint else { return }
// Get the distance from the current tank location to the waypoint. guard let distanceResult = GeometryEngine.geodeticDistance( from: tankLocation, to: point, distanceUnit: .meters, azimuthUnit: .degrees, curveType: .geodesic ) else { return }
// Move toward waypoint a short distance. let locations = GeometryEngine.geodeticMove( [tankLocation], distance: 1, distanceUnit: .meters, azimuth: distanceResult.azimuth1.value, azimuthUnit: distanceResult.azimuth1.unit.angularUnit, curveType: .geodesic ) tankGraphic.geometry = locations.first
// Set tank graphic heading. if let heading = tankGraphic.attributes["heading"] as? Double { // Divide by 10 to make the animation more smooth. tankGraphic.setAttributeValue( heading + ((distanceResult.azimuth1.value - heading) / 10), forKey: "heading" ) }
// Reset waypoint to stop animation when within 5 meters of the waypoint. if distanceResult.distance.value <= 5 { waypoint = nil } } }}
private extension URL { /// The on-demand resource URL for the Bradley tank. static var bradleyTank: URL { Bundle.main.url(forResource: "bradle", withExtension: "3ds", subdirectory: "bradley_low_3ds")! }
/// A world elevation service from Terrain3D ArcGIS REST service. static var worldElevationService: URL { URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")! }
/// A scene service URL for buildings in Brest, France. static var brestBuildingsService: URL { URL(string: "https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Buildings_Brest/SceneServer/layers/0")! }}