Measure distance in scene

View on GitHub

Measure distances between two points in 3D.

Image of measure distance in scene

Use case

The distance measurement analysis allows you to add to your app the same interactive measuring experience found in ArcGIS Pro, City Engine, and the ArcGIS API for JavaScript. You can set the unit system of measurement (metric or imperial). The units automatically switch to one appropriate for the current scale.

How to use the sample

Choose a unit system for the measurement in the segmented control. Tap any location in the scene to start measuring. Then tap and drag to an end location, and lift your finger to complete the measure. Tap a new location to start a new measurement.

How it works

  1. Create an AnalysisOverlay object and add it to the analysis overlay collection of the SceneView object.
  2. Specify the start location and end location to create a LocationDistanceMeasurement object. Initially, the start and end locations can be the same point.
  3. Add the location distance measurement analysis to the analysis overlay.
  4. Use the for-await-in syntax to get the measurements updates from locationDistanceMeasurement.measurements asynchronous stream. You can get the new values for the directDistance, horizontalDistance, and verticalDistance from the measurements update.

Relevant API

  • AnalysisOverlay
  • LocationDistanceMeasurement

Additional information

The LocationDistanceMeasurement analysis only performs planar distance calculations. This may not be appropriate for large distances where the Earth's curvature must be considered.

Tags

3D, analysis, distance, measure

Sample Code

MeasureDistanceInSceneView.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
// 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 SwiftUI
import ArcGIS

struct MeasureDistanceInSceneView: View {
    /// The view model for the sample.
    @StateObject private var model = Model()

    /// A string for the direct distance of the location distance measurement.
    @State private var directDistanceText = "--"

    /// A string for the horizontal distance of the location distance measurement.
    @State private var horizontalDistanceText = "--"

    /// A string for the vertical distance of the location distance measurement.
    @State private var verticalDistanceText = "--"

    /// The unit system for the location distance measurement, selected by the picker.
    @State private var unitSystemSelection: UnitSystem = .metric

    /// The overlay instruction message text.
    @State private var instructionText: String = .startMessage

    var body: some View {
        VStack {
            SceneViewReader { sceneViewProxy in
                SceneView(scene: model.scene, analysisOverlays: [model.analysisOverlay])
                    .onSingleTapGesture { screenPoint, _ in
                        // Set the start and end locations when the screen is tapped.
                        Task {
                            guard let location = try? await sceneViewProxy.location(
                                fromScreenPoint: screenPoint
                            ) else { return }

                            if model.locationDistanceMeasurement.startLocation != model.locationDistanceMeasurement.endLocation {
                                model.locationDistanceMeasurement.startLocation = location
                                instructionText = .endMessage
                            } else {
                                instructionText = .startMessage
                            }
                            model.locationDistanceMeasurement.endLocation = location
                        }
                    }
                    .onDragGesture { _, _ in
                        // Drag gesture is active when user has first set the start location.
                        return model.locationDistanceMeasurement.startLocation == model.locationDistanceMeasurement.endLocation
                    } onChanged: { screenPoint, _ in
                        // Move the end location on drag gesture.
                        Task {
                            guard let location = try? await sceneViewProxy.location(
                                fromScreenPoint: screenPoint
                            ) else { return }
                            model.locationDistanceMeasurement.endLocation = location
                        }
                    } onEnded: { _, _ in
                        instructionText = .startMessage
                    }
                    .task {
                        // Set distance text when there is a measurements update.
                        for await measurements in model.locationDistanceMeasurement.measurements {
                            directDistanceText = measurements.directDistance.formatted(.distance)
                            horizontalDistanceText = measurements.horizontalDistance.formatted(.distance)
                            verticalDistanceText = measurements.verticalDistance.formatted(.distance)
                        }
                    }
                    .overlay(alignment: .top) {
                        // Instruction text.
                        Text(instructionText)
                            .frame(maxWidth: .infinity, alignment: .center)
                            .padding(8)
                            .background(.thinMaterial, ignoresSafeAreaEdges: .horizontal)
                    }
            }

            // Distance texts.
            Text("Direct: \(directDistanceText)")
            Text("Horizontal: \(horizontalDistanceText)")
            Text("Vertical: \(verticalDistanceText)")

            // Unit system picker.
            Picker("", selection: $unitSystemSelection) {
                Text("Imperial").tag(UnitSystem.imperial)
                Text("Metric").tag(UnitSystem.metric)
            }
            .pickerStyle(.segmented)
            .padding()
            .onChange(of: unitSystemSelection) { _ in
                model.locationDistanceMeasurement.unitSystem = unitSystemSelection
            }
        }
    }
}

private extension MeasureDistanceInSceneView {
    /// The view model for the sample.
    class Model: ObservableObject {
        /// A scene with an imagery basemap.
        let scene = {
            let scene = ArcGIS.Scene(basemapStyle: .arcGISTopographic)

            // 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
        }()

        /// An analysis overlay for location distance measurement.
        let analysisOverlay = AnalysisOverlay()

        /// The location distance measurement.
        let locationDistanceMeasurement = LocationDistanceMeasurement(
            startLocation: Point(x: -4.494677, y: 48.384472, z: 24.772694, spatialReference: .wgs84),
            endLocation: Point(x: -4.495646, y: 48.384377, z: 58.501115, spatialReference: .wgs84)
        )

        init() {
            // Set scene to the viewpoint specified by the location distance measurement.
            let lookAtPoint = Envelope(
                min: locationDistanceMeasurement.startLocation,
                max: locationDistanceMeasurement.endLocation
            ).center
            let camera = Camera(lookingAt: lookAtPoint, distance: 200, heading: 0, pitch: 45, roll: 0)
            scene.initialViewpoint = Viewpoint(latitude: .nan, longitude: .nan, scale: .nan, camera: camera)

            // Add location distance measurement to the analysis overlay to display it.
            analysisOverlay.addAnalysis(locationDistanceMeasurement)
        }
    }
}

private extension FormatStyle where Self == Measurement<UnitLength>.FormatStyle {
    /// The format style for the distances.
    static var distance: Self {
        .measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number.precision(.fractionLength(2)))
    }
}

private extension String {
    /// The user instruction message for setting the start location.
    static let startMessage = "Tap on the map to set the start location."

    /// The user instruction message for setting the end location.
    static let endMessage = "Tap and drag on the map to set the end location."
}

private extension URL {
    /// 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 the buildings in Brest, France.
    static var brestBuildingsService: URL {
        URL(string: "https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Buildings_Brest/SceneServer/layers/0")!
    }
}

#Preview {
    MeasureDistanceInSceneView()
}

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