Show geodesic path between two points

View on GitHubSample viewer app

Calculate a geodesic path between two points and measure its distance.

Image of geodesic operations

Use case

A geodesic distance provides an accurate, real-world distance between two points. Visualizing flight paths between cities is a common example of a geodesic operation since the flight path between two airports takes into account the curvature of the earth, rather than following the planar path between those points, which appears as a straight line on a projected map.

How to use the sample

Tap anywhere on the map. A line graphic will display the geodesic line between the two points. In addition, text that indicates the geodesic distance between the two points will be updated. Tap elsewhere and a new line will be created.

How it works

  1. Create a Point and display it as a Graphic.
  2. Obtain a new point when a tap occurs on the MapView and add this point as a graphic.
  3. Create a Polyline from the two points.
  4. Execute GeometryEngine.densifyGeodetic(...) by passing in the created polyline then create a graphic from the returned Geometry.
  5. Execute GeometryEngine.lengthGeodetic(...) by passing in the two points and display the returned length on the screen.

Relevant API

  • GeometryEngine.densifyGeodetic
  • GeometryEngine.lengthGeodetic

About the data

The Imagery basemap provides the global context for the displayed geodesic line.

Tags

densify, distance, geodesic, geodetic

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
/*
 * 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.showgeodesicpathbetweentwopoints

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.Color
import com.arcgismaps.geometry.LinearUnit
import com.arcgismaps.geometry.LinearUnitId
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.Polyline
import com.arcgismaps.geometry.GeodeticCurveType
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.symbology.SimpleLineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol
import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.esri.arcgismaps.sample.showgeodesicpathbetweentwopoints.databinding.ActivityMainBinding
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import kotlin.math.roundToInt

class MainActivity : AppCompatActivity() {

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

    // set up data binding for the mapView
    private val mapView by lazy {
        activityMainBinding.mapView
    }

    // shows the distance information as a textview
    private val infoTextView by lazy {
        activityMainBinding.infoTextView
    }

    // a red marker symbol for the location points
    private val locationMarkerSymbol by lazy {
        SimpleMarkerSymbol(
            SimpleMarkerSymbolStyle.Circle,
            Color.red,
            10f
        )
    }

    // the marker graphic for the starting location
    private val startingLocationMarkerGraphic by lazy {
        Graphic(startingPoint, locationMarkerSymbol)
    }

    // marker graphic for the destination
    private val endLocationMarkerGraphic by lazy {
        Graphic(symbol = locationMarkerSymbol)
    }

    // the geodesic path represented as line graphic
    private val geodesicPathGraphic by lazy {
        val lineSymbol = SimpleLineSymbol(SimpleLineSymbolStyle.Dash, Color.red, 5f)
        Graphic(symbol = lineSymbol)
    }

    // the unit of distance measurement in kilometers
    private val unitsOfMeasurement = LinearUnit(LinearUnitId.Kilometers)


    // starting location for the distance calculation
    private val startingPoint = Point(-73.7781, 40.6413, SpatialReference.wgs84())


    // creates a graphic overlay to draw all graphics
    private val graphicsOverlay = GraphicsOverlay()

    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.API_KEY)
        lifecycle.addObserver(mapView)

        // create and add a map with a navigation night basemap style
        val map = ArcGISMap(BasemapStyle.ArcGISImageryStandard).apply {
            initialViewpoint = Viewpoint(Point(34.77, -10.24), 20e7)
        }
        // configure mapView assignments
        mapView.apply {
            this.map = map
            // add the graphics overlay to the mapview
            graphicsOverlays.add(graphicsOverlay)
        }

        // add all our marker graphics to the graphics overlay
        graphicsOverlay.graphics.addAll(
            listOf(startingLocationMarkerGraphic, endLocationMarkerGraphic, geodesicPathGraphic)
        )

        lifecycleScope.launch {
            // check if the map has loaded successfully
            map.load().onSuccess {
                // capture and collect when the user taps on the screen
                mapView.onSingleTapConfirmed.collect { event ->
                    event.mapPoint?.let { point -> displayGeodesicPath(point) }
                }
            }.onFailure {
                // if map load failed, show the error
                showError("Error Loading Map")
            }
        }
    }

    /**
     * Displays the destination location marker at the tapped location
     * and draws a geodesic path curve using GeometryEngine.densifyGeodetic
     * and computes the distance using GeometryEngine.lengthGeodetic
     */
    private fun displayGeodesicPath(point: Point) {
        // project the tapped point into the same spatial reference as source point
        val destinationPoint = GeometryEngine.projectOrNull(point, SpatialReference.wgs84())
            ?: return showError("Error converting point projection")

        // check if the destination point is within the map bounds
        // isEmpty returns true if out of bounds
        if (!destinationPoint.isEmpty) {
            // update the end location marker location on map
            endLocationMarkerGraphic.geometry = destinationPoint
            // create a polyline between source and destination points
            val polyline = Polyline(listOf(startingPoint, destinationPoint))
            // generate a geodesic curve using the polyline
            val pathGeometry = GeometryEngine.densifyGeodeticOrNull(
                geometry = polyline,
                maxSegmentLength = 1.0,
                lengthUnit = unitsOfMeasurement,
                curveType = GeodeticCurveType.Geodesic
                // only compute the distance if the returned curved path geometry is not null
            ) ?: return showError("Error creating a densified geometry")
            // update the path graphic
            geodesicPathGraphic.geometry = pathGeometry
            // compute the path distance in kilometers
            val distance = GeometryEngine.lengthGeodetic(
                geometry = pathGeometry,
                lengthUnit = unitsOfMeasurement,
                curveType = GeodeticCurveType.Geodesic
            )
            // display the distance result
            infoTextView.text = getString(R.string.distance_info_text, distance.roundToInt())
        }
    }

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

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