Show an exploratory line of sight between two moving objects.

Use case
An exploratory line of sight between geoelements (i.e. observer and target) will not remain constant whilst one or both are on the move.
An ExploratoryGeoElementLineOfSight is therefore useful in cases where visibility between two geoelements requires monitoring over a period of time in a partially obstructed field of view (such as buildings in a city).
Note: This analysis is a form of “exploratory analysis”, which means the results are calculated on the current scale of the data, and the results are generated very quickly but not persisted.
How to use the sample
An exploratory line of sight will display between a point on the Empire State Building (observer) and a taxi (target). The taxi will drive around a block and the exploratory line of sight should automatically update. The taxi will be highlighted and blinking when it is visible. A red segment on the line means the view between observer and target is obstructed, whereas cyan means the view is unobstructed. You can change the observer height with the slider to see how it affects the target’s visibility.
How it works
- Instantiate an
AnalysisOverlayand add it to theSceneView’s analysis overlays collection. - Instantiate an
ExploratoryGeoElementLineOfSight, passing in observer and targetGeoElements (features or graphics). Add the exploratory line of sight to the analysis overlay’s analysis collection. - To get the target visibility when it changes, react to the target visibility changing on the
ExploratoryGeoElementLineOfSightinstance.
Relevant API
- AnalysisOverlay
- ExploratoryGeoElementLineOfSight
- ExploratoryLineOfSightTargetVisibility
Additional information
This sample uses the GeoView-Compose Toolkit module to be able to implement a composable SceneView.
Tags
3D, exploratory line of sight, geoview-compose, visibility, visibility analysis
Sample Code
/* Copyright 2024 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.showexploratorylineofsightbetweengeoelements
import android.content.Intentimport android.os.Bundleimport com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
class DownloadActivity : DownloaderActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadAndStartSample( Intent(this, MainActivity::class.java), // get the app name of the sample getString(R.string.show_exploratory_line_of_sight_between_geoelements_app_name), listOf( "https://www.arcgis.com/home/item.html?id=3af5cfec0fd24dac8d88aea679027cb9" )
) }}/* Copyright 2024 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.showexploratorylineofsightbetweengeoelements
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.showexploratorylineofsightbetweengeoelements.screens.MainScreen
class MainActivity : ComponentActivity() {
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)
enableEdgeToEdge() setContent { SampleAppTheme { SampleApp() } } }
@Composable private fun SampleApp() { Surface( color = MaterialTheme.colorScheme.background ) { MainScreen( sampleName = getString(R.string.show_exploratory_line_of_sight_between_geoelements_app_name) ) } }}/* Copyright 2024 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.showexploratorylineofsightbetweengeoelements.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.core.content.ContextCompat.getStringimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.Colorimport com.arcgismaps.analysis.interactive.ExploratoryGeoElementLineOfSightimport com.arcgismaps.analysis.interactive.ExploratoryLineOfSightTargetVisibilityimport com.arcgismaps.geometry.AngularUnitimport com.arcgismaps.geometry.GeodeticCurveTypeimport com.arcgismaps.geometry.GeometryEngineimport com.arcgismaps.geometry.LinearUnitimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.PointBuilderimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.ArcGISTiledElevationSourceimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Surfaceimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.layers.ArcGISSceneLayerimport com.arcgismaps.mapping.symbology.ModelSceneSymbolimport com.arcgismaps.mapping.symbology.SceneSymbolAnchorPositionimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.symbology.SimpleRendererimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.Cameraimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.SurfacePlacementimport com.esri.arcgismaps.sample.showexploratorylineofsightbetweengeoelements.Rimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launchimport java.io.Fileimport kotlin.concurrent.timer
class SceneViewModel(private var application: Application) : AndroidViewModel(application) {
// Keep track of target visibility status string state. var targetVisibilityString by mutableStateOf("") private set
// Set visibility status string in the UI. private fun updateTargetVisibilityString(targetVisibility: String) { targetVisibilityString = targetVisibility }
// Initialize z to 50 as starting point and emit its state changes private val _observerHeight = MutableStateFlow(50.0) val observerHeight: StateFlow<Double> = _observerHeight.asStateFlow()
// Keeps track of wayPoints private var waypointsIndex = 0
// Create waypoints around a block for the taxi to drive to private val wayPoints = listOf( Point(-73.984513, 40.748469, SpatialReference.wgs84()), Point(-73.985068, 40.747786, SpatialReference.wgs84()), Point(-73.983452, 40.747091, SpatialReference.wgs84()), Point(-73.982961, 40.747762, SpatialReference.wgs84()), )
private val provisionPath: String by lazy { application.getExternalFilesDir(null)?.path.toString() + File.separator + application.getString( R.string.show_exploratory_line_of_sight_between_geoelements_app_name ) + File.separator }
private val filePath = provisionPath + application.getString(R.string.dolmus_model)
// Create a symbol of a taxi using the model file private val taxiSymbol = ModelSceneSymbol( uri = filePath, scale = 3.0F ).apply { anchorPosition = SceneSymbolAnchorPosition.Bottom }
// Create a graphic of a taxi to be the target private val taxiGraphic = Graphic( geometry = wayPoints[0], symbol = taxiSymbol ).apply { attributes["HEADING"] = 0.0 }
// Create a graphic near the Empire State Building to be the observer private val observerGraphic = Graphic( geometry = Point( x = -73.9853, y = 40.7484, z = 50.0, spatialReference = SpatialReference.wgs84() ), symbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Circle, color = Color.red, size = 5f ) )
// Zoom to show the observer private val camera = Camera( lookAtPoint = observerGraphic.geometry as Point, distance = 700.0, roll = 0.0, pitch = 45.0, heading = -30.0, )
// Define base surface for elevation data private val surface = Surface().apply { elevationSources.add( ArcGISTiledElevationSource( uri = getString( application, R.string.elevation_service_url ) ) ) }
// Define a scene layer for the New York buildings private val buildings = ArcGISSceneLayer(uri = application.getString(R.string.new_york_buildings_service_url))
// Create a scene and add a basemap to it. // Set the surface and buildings in the scene, and define the viewpoint on launch val scene = ArcGISScene(BasemapStyle.ArcGISTopographic).apply { baseSurface = surface operationalLayers.add(buildings) initialViewpoint = Viewpoint( boundingGeometry = observerGraphic.geometry as Point, camera = camera ) }
// Set up a heading expression to handle graphic rotation private val renderer3D = SimpleRenderer().apply { sceneProperties.headingExpression = ("[HEADING]") }
// Create graphic overlay to hold graphics // Set the surface placement, renderer, and add graphics, val graphicsOverlay = GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.RelativeToScene renderer = renderer3D graphics.addAll(listOf(observerGraphic, taxiGraphic)) }
// Create an exploratory line of sight between the two graphics and add it to the analysis overlay private val lineOfSight = ExploratoryGeoElementLineOfSight( observerGeoElement = observerGraphic, targetGeoElement = taxiGraphic ).apply { // Observe the visibility status of the moving taxi viewModelScope.launch(Dispatchers.Main) {
// Update target visibility status and select (highlight) the taxi when the line of sight target visibility changes to visible targetVisibility.collect { targetVisibility -> when(targetVisibility) { is ExploratoryLineOfSightTargetVisibility.Visible -> { updateTargetVisibilityString("Visible") taxiGraphic.isSelected = true } is ExploratoryLineOfSightTargetVisibility.Obstructed -> { updateTargetVisibilityString("Obstructed") taxiGraphic.isSelected = false } is ExploratoryLineOfSightTargetVisibility.Unknown -> { updateTargetVisibilityString("Unknown") taxiGraphic.isSelected = false } } } } }
// Create an analysis overlay to hold the line of sight val analysisOverlay = AnalysisOverlay().apply { analyses.add(lineOfSight) }
init {
// Create a timer to animate the tank timer( initialDelay = 0, period = 50, action = { animate() } ) }
/** * Updates elevation of the observer graphic using the given [height] */ fun updateHeight(height: Double) { val pointBuilder = PointBuilder(observerGraphic.geometry as Point).apply { z = height } observerGraphic.geometry = pointBuilder.toGeometry() _observerHeight.value = height }
/** * Moves the taxi toward the current waypoint a short distance. */ private fun animate() {
val meters = LinearUnit.meters val degrees = AngularUnit.degrees val waypoint = wayPoints[waypointsIndex] val location = taxiGraphic.geometry as Point
// Calculate the geodetic distance between current taxi location and next waypoint GeometryEngine.distanceGeodeticOrNull( point1 = location, point2 = waypoint, distanceUnit = meters, azimuthUnit = degrees, curveType = GeodeticCurveType.Geodesic )?.let { geodeticDistanceResult ->
taxiGraphic.apply {
// Move toward waypoint a short distance geometry = GeometryEngine.tryMoveGeodetic( pointCollection = listOf(location), distance = 1.0, distanceUnit = meters, azimuth = geodeticDistanceResult.azimuth1, azimuthUnit = degrees, curveType = GeodeticCurveType.Geodesic )[0]
// Rotate to the waypoint attributes["HEADING"] = geodeticDistanceResult.azimuth1
// Reached waypoint, move to next waypoint if (geodeticDistanceResult.distance <= 2) { waypointsIndex = (waypointsIndex + 1) % wayPoints.size } } } }
}/* Copyright 2024 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.showexploratorylineofsightbetweengeoelements.screens
import androidx.compose.foundation.backgroundimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.collectAsStateimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.showexploratorylineofsightbetweengeoelements.components.SceneViewModel
/** * Main screen layout for the sample app */@Composablefun MainScreen(sampleName: String) {
// Define the viewmodel of this sample val sceneViewModel: SceneViewModel = viewModel()
// Retrieve any changes to the z value from SceneViewModel val observerHeight = sceneViewModel.observerHeight.collectAsState().value.toInt()
// Defined in order to keep the z value in the positive range val offset = 100
Scaffold(topBar = { SampleTopAppBar(title = sampleName) }, content = { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(paddingValues), horizontalAlignment = Alignment.CenterHorizontally ) {
// Show the current target visibility status. Row( modifier = Modifier .fillMaxWidth() .padding(8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Text(text = "Target visibility status: ") Text( text = sceneViewModel.targetVisibilityString, color = if (sceneViewModel.targetVisibilityString.contains("Visible")) Color.Green else Color.Red ) } // Composable function that wraps the SceneView SceneView( modifier = Modifier .fillMaxWidth() .weight(1f), arcGISScene = sceneViewModel.scene, analysisOverlays = listOf(sceneViewModel.analysisOverlay), graphicsOverlays = listOf(sceneViewModel.graphicsOverlay) )
// Composable function that holds the slider and the text position value Row( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Text( text = "Observer height: ${(observerHeight + offset)}", ) Slider( modifier = Modifier.padding(start = 16.dp), value = (observerHeight.toFloat() + offset), valueRange = 0f..300f, onValueChange = { newHeight -> sceneViewModel.updateHeight(newHeight.toDouble() - offset) }, )
} } })}