Change camera controller

View on GitHubSample viewer app

Control the behavior of the camera in a scene.

Image of change camera controller

Use case

The globe camera controller (the default camera controller in all new scenes) allows a user to explore the scene freely by zooming in/out and panning around the globe. The orbit camera controllers fix the camera to look at a target location or geoelement. A primary use case is for following moving objects like cars and planes.

How to use the sample

The application loads with the default globe camera controller. To rotate and fix the scene around the plane, exit globe mode by choosing the "Orbit camera around plane" option (i.e. camera will now be fixed to the plane). Choose the "Orbit camera around location" option to rotate and center the scene around the location of the Upheaval Dome crater structure, or choose the "Free pan round the globe" option to return to default free navigation.

How it works

  1. Create an instance of a class extending CameraController: GlobeCameraController, OrbitLocationCameraController, OrbitGeoElementCameraController.
  2. Set the scene view's camera controller with sceneView.cameraController = cameraController.

Relevant API

  • ArcGISScene
  • Camera
  • GlobeCameraController
  • ModelSceneSymbol
  • OrbitGeoElementCameraController
  • OrbitLocationCameraController
  • SceneView

Tags

3D, camera, camera controller

Sample Code

MainActivity.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
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/*
 * 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
 *
 *    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.changecameracontroller

import android.os.Bundle
import android.util.Log
import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISScene
import com.arcgismaps.mapping.ArcGISTiledElevationSource
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.symbology.ModelSceneSymbol
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.SurfacePlacement
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.OrbitLocationCameraController
import com.arcgismaps.mapping.view.OrbitGeoElementCameraController
import com.arcgismaps.mapping.view.GlobeCameraController
import com.arcgismaps.mapping.view.Camera
import com.esri.arcgismaps.sample.changecameracontroller.databinding.ChangeCameraControllerActivityMainBinding
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.ensureActive
import java.io.File
import java.io.FileOutputStream

class MainActivity : AppCompatActivity() {

    // set up data binding for the activity
    private val activityMainBinding: ChangeCameraControllerActivityMainBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.change_camera_controller_activity_main)
    }

    private val sceneView by lazy {
        activityMainBinding.sceneView
    }

    // options dropdown view for the camera controller types
    private val cameraControllerOptionsView by lazy {
        // create an array adapter data source using the list of camera controller modes
        val arrayAdapter = ArrayAdapter(
            this,
            com.esri.arcgismaps.sample.sampleslib.R.layout.custom_dropdown_item,
            CameraControllerMode.getValuesByDisplayName()
        )
        activityMainBinding.bottomListItems.apply {
            setAdapter(arrayAdapter)
        }
    }

    // list of available asset files
    private val assetFiles by lazy {
        resources.getStringArray(R.array.asset_files).toList()
    }

    // the graphic representing the airplane 3d model
    private val airplane3DGraphic by lazy {
        // location for the target graphic
        val point = Point(-109.937516, 38.456714, 5000.0, SpatialReference.wgs84())
        // create the graphic with the target location
        Graphic(point)
    }

    // camera controller which orbits the plane graphic
    private val orbitPlaneCameraController by lazy {
        // instantiate a new camera controller with a distance from airplane graphic
        OrbitGeoElementCameraController(airplane3DGraphic, 100.0).apply {
            // set a relative pitch to the target
            setCameraPitchOffset(3.0)
            // set a relative heading to the target
            setCameraHeadingOffset(150.0)
        }
    }

    // camera controller which orbits a target location
    private val orbitLocationCameraController by lazy {
        // target location for the camera controller
        val point = Point(-109.929589, 38.437304, 1700.0, SpatialReference.wgs84())
        // instantiate a new camera controller with a distance from the target
        OrbitLocationCameraController(point, 5000.0).apply {
            // set a relative pitch to the target
            setCameraPitchOffset(3.0)
            // set a relative heading to the target
            setCameraHeadingOffset(150.0)
        }
    }

    // camera controller for free roam navigation
    private val globeCameraController = GlobeCameraController()

    // camera looking at the Upheaval Dome crater in Utah
    private val defaultCamera = Camera(
        latitude = 38.459291,
        longitude = -109.937576,
        altitude = 5500.0,
        heading = 150.0,
        pitch = 20.0,
        roll = 0.0
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // authentication with an API key or named user is
        // required to access basemaps and other location services
        ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
        lifecycle.addObserver(sceneView)

        // create and add a scene with an imagery basemap style
        val terrainScene = ArcGISScene(BasemapStyle.ArcGISImagery).apply {
            // add an elevation data source to the base surface
            baseSurface.elevationSources.add(
                ArcGISTiledElevationSource(getString(R.string.elevation_service_url))
            )
        }

        // graphics overlay for the scene to draw the 3d graphics on
        val graphicsOverlay = GraphicsOverlay().apply {
            // set the altitude values in the scene to be absolute
            sceneProperties.surfacePlacement = SurfacePlacement.Absolute
            // add the airplane 3d graphic to the graphics overlay
            graphics.add(airplane3DGraphic)
        }

        sceneView.apply {
            // set the scene to the SceneView
            scene = terrainScene
            // add the graphics overlay to the SceneView
            graphicsOverlays.add(graphicsOverlay)
        }

        lifecycleScope.launch {
            // if the map load failed show an error and return
            terrainScene.load().onFailure {
                showError("Failed to load the scene: ${it.message}")
                return@launch
            }
            // set the sceneView viewpoint to the default camera
            sceneView.setViewpointCamera(defaultCamera)
            // copy the airplane model assets to the cache directory if needed
            copyAssetsToCache(assetFiles, cacheDir, false)
            // load the airplane model file and update the the airplane3DGraphic
            loadModel(getString(R.string.bristol_model_file), airplane3DGraphic)
        }

        // set the click listener for the options dropdown view
        cameraControllerOptionsView.setOnItemClickListener { parent, _, position, _ ->
            // get the selected camera mode item
            val selectedItem = parent.getItemAtPosition(position) as String
            // get the CameraControllerMode from the selected item
            val mode = CameraControllerMode.getValue(selectedItem)
            // update the camera controller
            setCameraControllerMode(mode)
        }
    }

    /**
     * Loads a [ModelSceneSymbol] from the [filename] in [getCacheDir] and updates the [graphic].
     */
    private suspend fun loadModel(filename: String, graphic: Graphic) {
        val modelFile = File(cacheDir, filename)
        if (modelFile.exists()) {
            // create a new ModelSceneSymbol with the file
            val modelSceneSymbol = ModelSceneSymbol(modelFile.path).apply {
                heading = 45f
            }
            // if the symbol load failed show and error and return
            modelSceneSymbol.load().onFailure {
                return showError("Error loading airplane model: ${it.message}")
            }
            // update the graphic's symbol
            graphic.symbol = modelSceneSymbol
        } else {
            showError("Error loading airplane model: file does not exist.")
        }
    }

    /**
     * Updates the SceneView's camera controller based on the [mode] specified.
     */
    private fun setCameraControllerMode(mode: CameraControllerMode) {
        sceneView.cameraController = when (mode) {
            CameraControllerMode.OrbitPlane -> orbitPlaneCameraController
            CameraControllerMode.OrbitLocation -> orbitLocationCameraController
            CameraControllerMode.Globe -> globeCameraController
        }
    }

    /**
     * Copies the list of [assets] files from the assets folder to a given [cache] directory. This
     * suspending function runs on the [Dispatchers.IO] context. If [overwrite] is true, any assets
     * already in the [cache] directory are overwritten, otherwise copy is skipped.
     */
    private suspend fun copyAssetsToCache(
        assets: List<String>,
        cache: File,
        overwrite: Boolean
    ) = withContext(Dispatchers.IO) {
        // get the AssetManager
        val assetManager = applicationContext.assets ?: return@withContext
        assets.forEach { assetName ->
            // check for cancellation before reading/writing the asset files
            ensureActive()
            try {
                // create an output file in the cache directory
                val outFile = File(cache, assetName)
                // if the output file doesn't exist or overwrite is enabled
                if (!outFile.exists() || overwrite) {
                    // create an input stream to the asset
                    assetManager.open(assetName).use { inputStream ->
                        // create an file output stream to the output file
                        FileOutputStream(outFile).use { outputStream ->
                            // copy the input file stream to the output file stream
                            inputStream.copyTo(outputStream)
                        }
                        Log.i(localClassName, "$assetName copied to cache.")
                    }
                } else {
                    Log.i(localClassName, "$assetName already in cache, skipping copy.")
                }
            } catch (exception: Exception) {
                showError("Error caching asset :${exception.message}")
            }
        }
    }

    private fun showError(message: String) {
        Log.e(localClassName, message)
        Snackbar.make(sceneView, message, Snackbar.LENGTH_SHORT).show()
    }
}

// enum to keep track of the selected camera controller mode set for the SceneView
enum class CameraControllerMode(val displayName: String) {
    OrbitPlane("Orbit camera around a plane model"),
    OrbitLocation("Orbit camera around a crater"),
    Globe("Free pan around the globe");

    companion object {
        /**
         * Returns a List containing the [displayName] property of this enum type.
         * */
        fun getValuesByDisplayName(): List<String> {
            return entries.map { cameraControllerMode ->
                cameraControllerMode.displayName
            }
        }

        /**
         * Returns the enum constant of this type with the specified [displayName] property.
         */
        fun getValue(displayName: String): CameraControllerMode {
            return entries.first {
                it.displayName == displayName
            }
        }
    }
}

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