Calculate a viewshed using a geoprocessing service, in this case showing what parts of a landscape are visible from points on mountainous terrain.
Use case
A viewshed is used to highlight what is visible from a given point. A viewshed could be created to show what a hiker might be able to see from a given point at the top of a mountain. Equally, a viewshed could also be created from a point representing the maximum height of a proposed wind turbine to see from what areas the turbine would be visible.
How to use the sample
Tap the map to see all areas visible from that point within a 15km radius. Tapping on an elevated area will highlight a larger part of the surrounding landscape. It may take a few seconds for the task to run and send back the results.
How it works
- Create an
AGSGeoprocessingTask
object with the URL set to a geoprocessing service endpoint. - Create an
AGSFeatureCollectionTable
object and add a newAGSFeature
object whose geometry is the viewshed's observerAGSPoint
. - Make an
AGSGeoprocessingParameters
object passing in the observer point. - Use the geoprocessing task to create an
AGSGeoprocessingJob
object with the parameters. - Start the job and wait for it to complete and return an
AGSGeoprocessingResult
object. - Get the resulting
AGSGeoprocessingFeatures
object. - Iterate through the viewshed features to use their geometry or display the geometry in a new
AGSGraphic
object.
Relevant API
- AGSFeatureCollectionTable
- AGSGeoprocessingFeatures
- AGSGeoprocessingJob
- AGSGeoprocessingParameters
- AGSGeoprocessingResult
- AGSGeoprocessingTask
Tags
geoprocessing, heat map, heatmap, viewshed
Sample Code
// Copyright 2017 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 ViewshedGeoprocessingViewController: UIViewController, AGSGeoViewTouchDelegate {
@IBOutlet var mapView: AGSMapView!
private var geoprocessingTask: AGSGeoprocessingTask!
private var geoprocessingJob: AGSGeoprocessingJob!
private var inputGraphicsOverlay = AGSGraphicsOverlay()
private var resultGraphicsOverlay = AGSGraphicsOverlay()
override func viewDidLoad() {
super.viewDidLoad()
// add the source code button item to the right of navigation bar
(self.navigationItem.rightBarButtonItem as! SourceCodeBarButtonItem).filenames = ["ViewshedGeoprocessingViewController"]
self.mapView.map = AGSMap(basemapStyle: .arcGISTopographic)
self.mapView.setViewpoint(AGSViewpoint(latitude: 45.3790902612337, longitude: 6.84905317262762, scale: 144447.638572))
self.mapView.touchDelegate = self
// renderer for graphics overlays
let pointSymbol = AGSSimpleMarkerSymbol(style: .circle, color: .red, size: 10)
let renderer = AGSSimpleRenderer(symbol: pointSymbol)
self.inputGraphicsOverlay.renderer = renderer
let fillColor = UIColor(red: 226 / 255.0, green: 119 / 255.0, blue: 40 / 255.0, alpha: 120 / 255.0)
let fillSymbol = AGSSimpleFillSymbol(style: .solid, color: fillColor, outline: nil)
self.resultGraphicsOverlay.renderer = AGSSimpleRenderer(symbol: fillSymbol)
// add graphics overlays to the map view
self.mapView.graphicsOverlays.addObjects(from: [self.resultGraphicsOverlay, self.inputGraphicsOverlay])
let viewshedURL = URL(string: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Elevation/ESRI_Elevation_World/GPServer/Viewshed")!
self.geoprocessingTask = AGSGeoprocessingTask(url: viewshedURL)
}
private func addGraphicForPoint(_ point: AGSPoint) {
// remove existing graphics
self.inputGraphicsOverlay.graphics.removeAllObjects()
// new graphic
let graphic = AGSGraphic(geometry: point, symbol: nil, attributes: nil)
// add new graphic to the graphics overlay
self.inputGraphicsOverlay.graphics.add(graphic)
}
private func calculateViewshed(at point: AGSPoint) {
// remove previous graphics
self.resultGraphicsOverlay.graphics.removeAllObjects()
// Cancel previous job
self.geoprocessingJob?.progress.cancel()
// the service requires input of rest data type GPFeatureRecordSetLayer
// which is AGSGeoprocessingFeatures in runtime
// in order to create an object of AGSGeoprocessingFeatures we need a featureSet
// for which we will create a featureCollectionTable (since feature collection table
// is a feature set) and add the geometry as a feature to that table.
// create feature collection table for point geometry
let featureCollectionTable = AGSFeatureCollectionTable(fields: [AGSField](), geometryType: .point, spatialReference: point.spatialReference)
// create a new feature and assign the geometry
let newFeature = featureCollectionTable.createFeature()
newFeature.geometry = point
// show progress hud
UIApplication.shared.showProgressHUD(message: "Adding Feature")
// add the new feature to the feature collection table
featureCollectionTable.add(newFeature) { [weak self] (error: Error?) in
// dismiss progress hud
UIApplication.shared.hideProgressHUD()
if let error = error {
// show error
self?.presentAlert(error: error)
} else {
self?.performGeoprocessing(featureCollectionTable)
}
}
}
private func performGeoprocessing(_ featureCollectionTable: AGSFeatureCollectionTable) {
// geoprocessing parameters
let params = AGSGeoprocessingParameters(executionType: .synchronousExecute)
params.processSpatialReference = featureCollectionTable.spatialReference
params.outputSpatialReference = featureCollectionTable.spatialReference
// use the feature collection table to create the required AGSGeoprocessingFeatures input
params.inputs["Input_Observation_Point"] = AGSGeoprocessingFeatures(featureSet: featureCollectionTable)
// initialize job from geoprocessing task
geoprocessingJob = geoprocessingTask.geoprocessingJob(with: params)
// start the job
geoprocessingJob.start(statusHandler: { (status: AGSJobStatus) in
// show progress hud with job status
UIApplication.shared.showProgressHUD(message: status.statusString())
}, completion: { [weak self] (result: AGSGeoprocessingResult?, error: Error?) in
// dismiss progress hud
UIApplication.shared.hideProgressHUD()
guard let self = self else {
return
}
if let error = error {
if (error as NSError).code != NSUserCancelledError { // if not cancelled
self.presentAlert(error: error)
}
} else {
// The service returns result in form of AGSGeoprocessingFeatures
// Cast the results and add the features from featureSet to graphics overlay
// in form of graphics
if let resultFeatures = result?.outputs["Viewshed_Result"] as? AGSGeoprocessingFeatures,
let featureSet = resultFeatures.features {
for feature in featureSet.featureEnumerator().allObjects {
let graphic = AGSGraphic(geometry: feature.geometry, symbol: nil, attributes: nil)
self.resultGraphicsOverlay.graphics.add(graphic)
}
}
}
})
}
// MARK: - AGSGeoViewTouchDelegate
func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
// add a graphic in graphics overlay for the tapped point
self.addGraphicForPoint(mapPoint)
// calculate viewshed
self.calculateViewshed(at: mapPoint)
}
}