Surface placements

View on GitHub
Sample viewer app

Position graphics relative to a surface using different surface placement modes.

Surface placements sample

Use case

Depending on the use case, data might be displayed at an absolute height (e.g. flight data recorded with altitude information), at a relative height to the terrain (e.g. transmission lines positioned relative to the ground), at a relative height to objects in the scene (e.g. extruded polygons, integrated mesh scene layer), or draped directly onto the terrain (e.g. location markers, area boundaries).

How to use the sample

The sample loads a scene showing four points that use the individual surface placement rules (absolute, relative, relative to scene, and either draped billboarded or draped flat). Use the toggle to change the draped mode and the slider to dynamically adjust the z value of the graphics. Explore the scene by zooming in/out and by panning around to observe the effects of the surface placement rules.

How it works

  1. Create an AGSGraphicsOverlay for each placement mode, specifying the surfacePlacement:

    • absolute, position graphic using only its z value.
    • relative, position graphic using its z value plus the elevation of the surface.
    • relativeToScene, position graphic using its z value plus the altitude values of the scene.
    • drapedBillboarded, position graphic upright on the surface and always facing the camera, not using its z value.
    • drapedFlat, position graphic flat on the surface, not using its z value.
  2. Add graphics to the graphics overlay's graphics array.
  3. Add each graphics overlay to the scene view.

Relevant API

  • AGSGraphic
  • AGSGraphicsOverlay
  • AGSLayerSceneProperties
  • AGSSurface
  • AGSSurfacePlacement
  • class AGSGeometryEngine.geometry(bySettingZ:in:)

About the data

The scene shows a view of Brest, France. Four points are shown hovering with positions defined by each of the different surface placement modes (absolute, relative, relative to scene, and either draped billboarded or draped flat).

Additional information

This sample uses an elevation service to add elevation/terrain to the scene. Graphics are positioned relative to that surface for the drapedBillboarded, drapedFlat, and relative surface placement modes. It also uses a scene layer containing 3D models of buildings. Graphics are positioned relative to that scene layer for the relativeToScene surface placement mode.

Tags

3D, absolute, altitude, draped, elevation, floating, relative, scenes, sea level, surface placement

Sample Code

SurfacePlacementsViewController.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
//
// Copyright 2016 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

/// A view controller that manages the interface of the Surface Placements
/// sample.
class SurfacePlacementsViewController: UIViewController {
    // MARK: Instance properties

    /// A label to show the value of the slider.
    @IBOutlet weak var zValueLabel: UILabel!
    /// The slider to change z-value of `AGSPoint` geometries, from 0 to 140 in meters.
    @IBOutlet weak var zValueSlider: UISlider! {
        didSet {
            zValueSlider.value = (zValueSlider.maximumValue + zValueSlider.minimumValue) / 2
        }
    }
    /// The segmented control to toggle the visibility of two draped mode graphics overlays.
    @IBOutlet weak var drapedModeSegmentedControl: UISegmentedControl!

    /// The scene view managed by the view controller.
    @IBOutlet var sceneView: AGSSceneView! {
        didSet {
            sceneView.scene = makeScene()
            sceneView.setViewpointCamera(AGSCamera(latitude: 48.3889, longitude: -4.4595, altitude: 80, heading: 330, pitch: 97, roll: 0))
            // Add graphics overlays of different surface placement modes to the scene.
            let surfacePlacements: [AGSSurfacePlacement] = [
                .drapedBillboarded,
                .drapedFlat,
                .relative,
                .relativeToScene,
                .absolute
            ]
            let overlays = surfacePlacements.map(makeGraphicsOverlay)
            overlaysBySurfacePlacement = Dictionary(uniqueKeysWithValues: zip(surfacePlacements, overlays))
            sceneView.graphicsOverlays.addObjects(from: overlays)
        }
    }

    /// A dictionary for graphics overlays of different surface placement modes.
    var overlaysBySurfacePlacement = [AGSSurfacePlacement: AGSGraphicsOverlay]()
    /// A formatter to format z-value strings.
    let zValueFormatter: MeasurementFormatter = {
        let formatter = MeasurementFormatter()
        formatter.unitStyle = .short
        formatter.unitOptions = .naturalScale
        formatter.numberFormatter.maximumFractionDigits = 0
        return formatter
    }()

    // MARK: - Actions

    @IBAction func segmentedControlValueChanged(_ sender: UISegmentedControl) {
        // Toggle the visibility of two draped mode graphics overlays respectively.
        let isDrapedFlat = sender.selectedSegmentIndex == 1
        overlaysBySurfacePlacement[.drapedFlat]!.isVisible = isDrapedFlat
        overlaysBySurfacePlacement[.drapedBillboarded]!.isVisible = !isDrapedFlat
    }

    @IBAction func sliderValueChanged(_ sender: UISlider) {
        let zValue = Double(sender.value)
        zValueLabel.text = zValueFormatter.string(from: Measurement<UnitLength>(value: zValue, unit: .meters))
        // Set the z-value of each geometry of surface placement graphics.
        overlaysBySurfacePlacement.values.forEach { graphicOverlay in
            graphicOverlay.graphics.forEach { graphic in
                let g = graphic as! AGSGraphic
                g.geometry = AGSGeometryEngine.geometry(bySettingZ: zValue, in: g.geometry!)
            }
        }
    }

    // MARK: Initialize scene and make graphics overlays

    /// Create a scene.
    ///
    /// - Returns: A new `AGSScene` object.
    func makeScene() -> AGSScene {
        let scene = AGSScene(basemap: .imagery())
        // Add a base surface for elevation data.
        let surface = AGSSurface()
        // Create elevation source from the Terrain 3D ArcGIS REST Service.
        let worldElevationServiceURL = URL(string: "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")!
        let elevationSource = AGSArcGISTiledElevationSource(url: worldElevationServiceURL)
        surface.elevationSources.append(elevationSource)
        // Create scene layer from the Brest, France scene server.
        let sceneServiceURL = URL(string: "https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Buildings_Brest/SceneServer")!
        let sceneLayer = AGSArcGISSceneLayer(url: sceneServiceURL)
        scene.baseSurface = surface
        scene.operationalLayers.add(sceneLayer)
        return scene
    }

    /// Create a graphics overlay for the given surface placement.
    ///
    /// - Parameter surfacePlacement: The surface placement for which to create a graphics overlay.
    /// - Returns: A new `AGSGraphicsOverlay` object.
    func makeGraphicsOverlay(surfacePlacement: AGSSurfacePlacement) -> AGSGraphicsOverlay {
        let markerSymbol = AGSSimpleMarkerSymbol(style: .triangle, color: .red, size: 20)
        let textSymbol = AGSTextSymbol(text: surfacePlacement.title, color: .blue, size: 20, horizontalAlignment: .left, verticalAlignment: .middle)
        // Add offset to avoid overlapping text and marker.
        textSymbol.offsetY = 20
        // Add offset to x and y of the geometry, to better differentiate certain geometries.
        let offset = surfacePlacement == .relativeToScene ? 2e-4 : 0
        let surfaceRelatedPoint = AGSPoint(x: -4.4609257 + offset, y: 48.3903965 + offset, z: 70, spatialReference: .wgs84())
        let graphics = [markerSymbol, textSymbol].map { AGSGraphic(geometry: surfaceRelatedPoint, symbol: $0) }
        let graphicsOverlay = AGSGraphicsOverlay()
        graphicsOverlay.sceneProperties?.surfacePlacement = surfacePlacement
        graphicsOverlay.graphics.addObjects(from: graphics)
        return graphicsOverlay
    }

    // 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 = ["SurfacePlacementsViewController"]
        // Initialize the slider and draped mode visibility.
        segmentedControlValueChanged(drapedModeSegmentedControl)
        sliderValueChanged(zValueSlider)
    }
}

private extension AGSSurfacePlacement {
    /// The human readable name of the surface placement.
    var title: String {
        switch self {
        case .drapedBillboarded: return "Draped Billboarded"
        case .absolute: return "Absolute"
        case .relative: return "Relative"
        case .relativeToScene: return "Relative to Scene"
        case .drapedFlat: return "Draped Flat"
        @unknown default: return "Unknown"
        }
    }
}

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