Skip to content

Augment reality to collect data

View on GitHubSample viewer app

Tap on real-world objects to collect data.

Image of augment reality to collect data

Use case

You can use AR to quickly photograph an object and automatically determine the object's real-world location, facilitating a more efficient data collection workflow. For example, you could quickly catalog trees in a park, while maintaining visual context of which trees have been recorded - no need for spray paint or tape.

How to use the sample

Before you start, ensure the device has good satellite visibility (ie. no trees or ceilings overhead) or, if using WorldScaleTrackingMode.Geospatial, that the device is outside in an area with VPS availability. This sample will indicate whether the device has VPS availability when in Geospatial tracking mode.

When you tap, a yellow diamond will appear at the tapped location. You can move around to visually verify that the tapped point is in the correct physical location. When you're satisfied, tap the '+' button to record the feature.

How it works

  1. Add a WorldScaleSceneView composable to the augmented reality screen, available in the ArcGIS Maps SDK for Kotlin toolkit.
    • The component is available both in World tracking and Geospatial tracking modes. Geospatial tracking uses street view data to calibrate augmented reality positioning and is available with an ARCORE API key.
  2. Load the feature service, create feature layer and add it to the scene.
  3. Create and add the elevation surface to the scene.
  4. Create a graphics overlay for planning the location of features to add and add it to the scene.
  5. Use the onSingleTapConfirmed lambda parameter on the WorldScaleSceneView to detect when the user taps and get the real-world location of the point they tapped.
  6. Add a graphic to the graphics overlay preview where the feature will be placed and allow the user to visually verify the placement.
  7. Prompt the user for a tree health value, then create the feature.

Relevant API

  • GraphicsOverlay
  • SceneView
  • Surface
  • WorldScaleSceneView

About the data

The sample uses a publicly-editable sample tree survey feature service hosted on ArcGIS Online called AR Tree Survey. You can use AR to quickly record the location and health of a tree.

Additional information

This sample requires a device that is compatible with ARCore.

The onSingleTapConfirmed lambda parameter to the WorldScaleSceneView passes a mapPoint parameter when it is able to determine the real-world location of the tapped point. On devices that support ARCore's Depth API, this point is represents the closest visible object to the device at the tapped screen point in the camera feed. On devices that do not support the Depth API, ARCore will attempt to perform a hit test against any planes that were detected in the scene at that location. If no planes are detected, then mapPoint will be null.

Note that the WorldScaleSceneViewProxy also supports converting screen coordinates to scene points using WorldScaleSceneViewProxy.screenToBaseSurface() and WorldScaleSceneViewProxy.screenToLocation(). However, these methods will test the screen coordinate against virtual objects in the scene, so real-world objects that do not have geometry (ie. a mesh) will not be used for the calculation. Therefore, screenToBaseSurface() and screenToLocation() should only be used where the developer is sure that the data contains geometry for the real-world object in the camera feed.

This sample uses the onSingleTapConfirmed lambda, as it is the only way to get accurate positions for features present in the real-world but not present in the scene, such as trees.

Note that unlike other scene samples, a basemap isn't shown most of the time, because the real world provides the context. Only while calibrating is the basemap displayed at 50% opacity, to give the user a visual reference to compare to.

World-scale AR is one of three main patterns for working with geographic information in augmented reality currently available in the toolkit.

Note that apps using ARCore must comply with ARCore's user privacy requirements. See this page for more information.

See the 'Edit feature attachments' sample for more specific information about the attachment editing workflow.

Tags

attachment, augmented reality, capture, collection, collector, data, field, field worker, full-scale, mixed reality, survey, world-scale

Sample Code

AugmentRealityToCollectDataViewModel.ktAugmentRealityToCollectDataViewModel.ktMainActivity.ktAugmentRealityToCollectDataScreen.kt
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
/* Copyright 2025 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.
 *
 */

package com.esri.arcgismaps.sample.augmentrealitytocollectdata.components

import android.app.Application
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.Color
import com.arcgismaps.data.ServiceFeatureTable
import com.arcgismaps.geometry.Point
import com.arcgismaps.mapping.ArcGISScene
import com.arcgismaps.mapping.Basemap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.ElevationSource
import com.arcgismaps.mapping.layers.FeatureLayer
import com.arcgismaps.mapping.symbology.SimpleMarkerSceneSymbol
import com.arcgismaps.mapping.symbology.SimpleMarkerSceneSymbolStyle
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.SingleTapConfirmedEvent
import com.arcgismaps.mapping.view.SurfacePlacement
import com.arcgismaps.toolkit.ar.WorldScaleSceneViewProxy
import com.arcgismaps.toolkit.ar.WorldScaleVpsAvailability
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch

class AugmentRealityToCollectDataViewModel(app: Application) : AndroidViewModel(app) {
    private val basemap = Basemap(BasemapStyle.ArcGISHumanGeography)
    // The AR tree survey service feature table
    private val featureTable = ServiceFeatureTable("https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/AR_Tree_Survey/FeatureServer/0")
    private val featureLayer = FeatureLayer.createWithFeatureTable(featureTable)
    val arcGISScene = ArcGISScene(basemap).apply {
        // an elevation source is required for the scene to be placed at the correct elevation
        // if not used, the scene may appear far below the device position because the device position
        // is calculated with elevation
        baseSurface.elevationSources.add(ElevationSource.fromTerrain3dService())
        baseSurface.backgroundGrid.isVisible = false
        baseSurface.opacity = 0.0f
        // add the AR tree survey feature layer.
        operationalLayers.add(featureLayer)
    }

    // The graphics overlay which shows marker symbols.
    val graphicsOverlay = GraphicsOverlay().apply {
        sceneProperties.surfacePlacement = SurfacePlacement.Absolute
    }

    var isVpsAvailable by mutableStateOf(false)

    val worldScaleSceneViewProxy = WorldScaleSceneViewProxy()

    // Create a message dialog view model for handling error messages
    val messageDialogVM = MessageDialogViewModel()

    var isDialogOptionsVisible by mutableStateOf(false)
        private set

    // The current marker graphic representing the user's selection
    private var treeMarker : Graphic? = null

    // A MutableSharedFlow that emits Point locations of the viewpoint camera
    val viewpointCameraLocationFlow = MutableSharedFlow<Point>(
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    init {
        viewModelScope.launch {
            arcGISScene.load().onFailure { messageDialogVM.showMessageDialog(it) }
        }
        periodicallyPollVpsAvailability()
    }

    // Adds a marker to the graphics overlay based on a single tap event
    fun addMarker(singleTapConfirmedEvent: SingleTapConfirmedEvent) {
        // Remove all graphics from the graphics overlay
        graphicsOverlay.graphics.clear()
        singleTapConfirmedEvent.mapPoint.let { point ->
            // Create a new marker graphic at the specified point with a diamond symbol
            val newMarker = Graphic(
                point,
                SimpleMarkerSceneSymbol(
                    SimpleMarkerSceneSymbolStyle.Diamond,
                    Color.yellow,
                    height = 1.0,
                    width = 1.0,
                    depth = 1.0
                )
            )
            treeMarker = newMarker
            graphicsOverlay.graphics.add(newMarker)
        }
    }

    // Adds a feature to represent a tree to the tree survey service feature table.
    fun addTree(context: Context, health: TreeHealth){
        treeMarker?.let { treeMarker ->
            // Set up the feature attributes
            val featureAttributes = mapOf<String, Any>(
                "Health" to health.value,
                "Height" to 3.2,
                "Diameter" to 1.2,
            )

            // Retrieve the marker's geometry as a Point
            val point = (treeMarker.geometry as? Point) ?: run {
                messageDialogVM.showMessageDialog("Something went wrong")
                return@let
            }

            // Create a new feature at the point
            val feature = featureTable.createFeature(featureAttributes, point)

            // Add the feature to the feature table
            viewModelScope.launch {
                featureTable.addFeature(feature)
                    .onSuccess {
                        // Upload changes from the local feature table to the feature service
                        featureTable.applyEdits()
                            .onSuccess { showToast(context, "Successfully added tree data!")}
                            .onFailure { e -> messageDialogVM.showMessageDialog(e) }
                    }.onFailure { e -> messageDialogVM.showMessageDialog(e) }
            }

            // Resets the feature's attributes and geometry to match the data source, discarding unsaved changes.
            feature.refresh()
        }
    }

    // Emits the camera location if it is not at (0.0, 0.0).
    fun onCurrentViewpointCameraChanged(cameraLocation: Point){
        if (cameraLocation.x != 0.0 && cameraLocation.y != 0.0) {
            viewpointCameraLocationFlow.tryEmit(cameraLocation)
        }
    }

    // Collects viewpoint camera locations once in 10 seconds and checks for VPS availability
    private fun periodicallyPollVpsAvailability(){
        viewModelScope.launch {
            viewpointCameraLocationFlow
                .sample(10_000)
                .collect { location ->
                    worldScaleSceneViewProxy.checkVpsAvailability(location.y, location.x).onSuccess {
                        isVpsAvailable = it == WorldScaleVpsAvailability.Available
                    }
                }
        }
    }

    /**
     * Displays a dialog for adding tree data if a marker exists
     */
    fun showDialog(context: Context){
        if (treeMarker == null) {
            showToast(context, "Please create marker by tapping on the screen")
            return
        }
        isDialogOptionsVisible = true
    }

    fun hideDialog(){
        isDialogOptionsVisible = false
    }
}

/**
 * Represents the health status of a tree.
 *
 * @property value The numerical value associated with the health status.
 */
enum class TreeHealth(val value: Short){
    Dead(0),
    Distressed(5),
    Healthy(10),
}

private fun showToast(context: Context, message: String) {
    Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}

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