Skip to content
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. Tap and hold on a location in the scene to start measuring. While continuing to hold, drag to position the end location.

How it works

  1. Specify the start location and end location to create an ExploratoryLocationDistanceMeasurement object.
  2. Create an AnalysisOverlay and add it to the analysis overlay collection of the ArcGISSceneViewController.
  3. Add the location distance measurement analysis to the analysis overlay.
  4. The onMeasurementChanged callback will trigger if the distances change. You can get the new values for the directDistance, horizontalDistance, and verticalDistance from the stream data passed through the callback.

Relevant API

  • AnalysisOverlay
  • ExploratoryLocationDistanceMeasurement

Additional information

The ExploratoryLocationDistanceMeasurement 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

measure_distance_in_scene.dart
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
// Copyright 2026 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 'package:arcgis_maps/arcgis_maps.dart';
import 'package:arcgis_maps_sdk_flutter_samples/common/common.dart';
import 'package:flutter/material.dart';

class MeasureDistanceInScene extends StatefulWidget {
  const MeasureDistanceInScene({super.key});

  @override
  State<MeasureDistanceInScene> createState() => _MeasureDistanceInSceneState();
}

class _MeasureDistanceInSceneState extends State<MeasureDistanceInScene>
    with SampleStateSupport {
  // Create a controller for the scene view.
  final _sceneViewController = ArcGISSceneView.createController();

  // Create the distance measurement analysis with initial locations in Brest, France.
  final _locationDistanceMeasurement = ExploratoryLocationDistanceMeasurement(
    startLocation: ArcGISPoint(
      x: -4.494677,
      y: 48.384472,
      z: 24.772694,
      spatialReference: .wgs84,
    ),
    endLocation: ArcGISPoint(
      x: -4.495646,
      y: 48.384377,
      z: 58.501115,
      spatialReference: .wgs84,
    ),
  );

  // A variable to track the current state of the measurement process.
  var _measurementState = MeasurementState.setStartLocation;

  // A flag for when the scene view is ready and controls can be used.
  var _ready = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        top: false,
        left: false,
        right: false,
        child: Stack(
          children: [
            Column(
              children: [
                Expanded(
                  child: Stack(
                    children: [
                      // Add a scene view to the widget tree and set a controller.
                      ArcGISSceneView(
                        controllerProvider: () => _sceneViewController,
                        onSceneViewReady: onSceneViewReady,
                      ),
                      // Add a detector to handle long-press gestures for changing the measurement.
                      GestureDetector(
                        onLongPressStart: onLongPressStart,
                        onLongPressMoveUpdate: onLongPressMoveUpdate,
                        onLongPressEnd: onLongPressEnd,
                      ),
                    ],
                  ),
                ),
                // Add a StreamBuilder to listen to changes in the measurement and display the distance values.
                StreamBuilder(
                  stream: _locationDistanceMeasurement.onMeasurementChanged,
                  builder: (context, snapshot) {
                    final measurement = snapshot.data;
                    if (measurement == null) {
                      return const SizedBox.shrink();
                    }

                    return Column(
                      children: [
                        // Display the formatted distance values.
                        Text('Direct: ${measurement.directDistance.formatted}'),
                        Text(
                          'Horizontal: ${measurement.horizontalDistance.formatted}',
                        ),
                        Text(
                          'Vertical: ${measurement.verticalDistance.formatted}',
                        ),
                        // Add a segmented button to switch between unit systems.
                        SegmentedButton(
                          segments: [
                            ...UnitSystem.values.map(
                              (unitSystem) => ButtonSegment(
                                value: unitSystem,
                                label: Text(unitSystem.name.capitalized),
                              ),
                            ),
                          ],
                          selected: {_locationDistanceMeasurement.unitSystem},
                          onSelectionChanged: (newSelection) {
                            setState(
                              () => _locationDistanceMeasurement.unitSystem =
                                  newSelection.first,
                            );
                          },
                        ),
                      ],
                    );
                  },
                ),
              ],
            ),
            // Display a banner with instructions at the top.
            SafeArea(
              left: false,
              right: false,
              child: IgnorePointer(
                child: Container(
                  padding: const EdgeInsets.all(10),
                  color: Colors.white.withValues(alpha: 0.7),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        _measurementState.instructions,
                        textAlign: TextAlign.center,
                        style: Theme.of(context).textTheme.labelMedium,
                      ),
                    ],
                  ),
                ),
              ),
            ),
            // Display a progress indicator and prevent interaction until state is ready.
            LoadingIndicator(visible: !_ready),
          ],
        ),
      ),
    );
  }

  Future<void> onSceneViewReady() async {
    // Create a scene with a topographic basemap style.
    final scene = ArcGISScene.withBasemapStyle(.arcGISTopographic);
    _sceneViewController.arcGISScene = scene;

    // Set the initial camera position to look at the distance measurement locations.
    final lookAtPoint = Envelope.fromPoints(
      _locationDistanceMeasurement.startLocation,
      _locationDistanceMeasurement.endLocation,
    ).center;
    final camera = Camera.withLookAtPoint(
      lookAtPoint: lookAtPoint,
      distance: 200,
      heading: 0,
      pitch: 45,
      roll: 0,
    );
    scene.initialViewpoint = Viewpoint.withPointScaleCamera(
      center: lookAtPoint,
      scale: 1,
      camera: camera,
    );

    // Add the world elevation source to the scene's surface.
    final elevationSource = ArcGISTiledElevationSource.withUri(
      Uri.parse(
        'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer',
      ),
    );
    scene.baseSurface.elevationSources.add(elevationSource);

    // Add a scene layer of 3D buildings in Brest, France.
    final buildingsLayer = ArcGISSceneLayer.withUri(
      Uri.parse(
        'https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Buildings_Brest/SceneServer/layers/0',
      ),
    );
    scene.operationalLayers.add(buildingsLayer);

    // Create an AnalysisOverlay and add the measurement analysis to it.
    final analysisOverlay = AnalysisOverlay();
    analysisOverlay.analyses.add(_locationDistanceMeasurement);

    // Add the AnalysisOverlay to the view controller.
    _sceneViewController.analysisOverlays.add(analysisOverlay);

    // Set the ready state variable to true to enable the sample UI.
    setState(() => _ready = true);
  }

  Future<void> onLongPressStart(LongPressStartDetails details) async {
    // Convert the screen point to a map point and set it as the start and end locations of the measurement.
    final mapPoint = await _sceneViewController.screenToLocation(
      screen: details.localPosition,
    );
    _locationDistanceMeasurement.startLocation = mapPoint;
    _locationDistanceMeasurement.endLocation = mapPoint;

    // Update the measurement state to be setting the end location.
    setState(() => _measurementState = MeasurementState.setEndLocation);
  }

  Future<void> onLongPressMoveUpdate(LongPressMoveUpdateDetails details) async {
    // Convert the screen point to a map point and update the end location of the measurement.
    final mapPoint = await _sceneViewController.screenToLocation(
      screen: details.localPosition,
    );
    _locationDistanceMeasurement.endLocation = mapPoint;
  }

  void onLongPressEnd(LongPressEndDetails details) {
    // Return the measurement state back to setting the start location.
    setState(() => _measurementState = MeasurementState.setStartLocation);
  }
}

// An enum to track the state of the long-press-and-drag measurement sequence.
enum MeasurementState {
  setStartLocation(instructions: 'Tap and hold to set the start location.'),
  setEndLocation(instructions: 'While holding, drag to set the end location.');

  const MeasurementState({required this.instructions});

  final String instructions;
}

extension on Distance {
  // A helper method to format the distance value with its unit for display.
  String get formatted => '${value.toStringAsFixed(2)} ${unit.abbreviation}';
}

extension on String {
  // A helper method to capitalize the first letter of a string.
  String get capitalized =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}

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