Find route in transport network

View on GitHubSample viewer app

Solve a route on-the-fly using offline data.

Image of find route in transport network

Use case

You can use an offline network to enable routing in disconnected scenarios. For example, you could provide offline location capabilities to field workers repairing critical infrastructure in a disaster when network availability is limited.

How to use the sample

Tap once near a road to add a stop to the map. A number graphic will show its order in the route. After adding at least 2 stops, a route will display. Choose "Fastest" or "Shortest" from the switch at the top of the screen to control how the route is optimized. The route will update on-the-fly while moving stops. The green box marks the boundary of the routable area provided by the offline data.

How it works

  1. Create the map's Basemap from a local tile package using a TileCache and ArcGISTiledLayer.
  2. Create a RouteTask with an offline locator geodatabase.
  3. Get the RouteParameters using routeTask.createDefaultParameters().
  4. Create Stops and add them to the route task's parameters using RouteParameters.setStops(...).
  5. Solve the Route using routeTask.solveRoute(routeParameters).
  6. Create a graphic with the route's geometry and a SimpleLineSymbol and display it on another GraphicsOverlay.

Relevant API

  • RouteParameters
  • RouteResult
  • RouteTask
  • Stop
  • TravelMode

About the data

This sample uses a pre-packaged sample dataset consisting of a geodatabase with a San Diego road routing network and a tile package with a streets basemap.

Tags

connectivity, disconnected, fastest, locator, navigation, network analysis, offline, routing, shortest, turn-by-turn

Sample Code

MainActivity.ktMainActivity.ktDownloadActivity.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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/* 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.findrouteintransportnetwork

import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.lifecycleScope
import com.arcgismaps.ApiKey
import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.Color
import com.arcgismaps.geometry.Envelope
import com.arcgismaps.geometry.Geometry
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.Basemap
import com.arcgismaps.mapping.layers.ArcGISTiledLayer
import com.arcgismaps.mapping.layers.TileCache
import com.arcgismaps.mapping.symbology.CompositeSymbol
import com.arcgismaps.mapping.symbology.HorizontalAlignment
import com.arcgismaps.mapping.symbology.PictureMarkerSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
import com.arcgismaps.mapping.symbology.TextSymbol
import com.arcgismaps.mapping.symbology.VerticalAlignment
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.ScreenCoordinate
import com.arcgismaps.tasks.networkanalysis.RouteParameters
import com.arcgismaps.tasks.networkanalysis.RouteTask
import com.arcgismaps.tasks.networkanalysis.Stop
import com.esri.arcgismaps.sample.findrouteintransportnetwork.databinding.FindRouteInTransportNetworkActivityMainBinding
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import java.io.File
import kotlin.math.roundToInt

class MainActivity : AppCompatActivity() {

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

    private val provisionPath: String by lazy {
        getExternalFilesDir(null)?.path.toString() + File.separator + getString(R.string.find_route_in_transport_network_app_name)
    }

    private val mapView by lazy {
        activityMainBinding.mapView
    }

    private val toggleButtons by lazy {
        activityMainBinding.toggleButtons
    }

    private val clearButton by lazy {
        activityMainBinding.clearButton
    }

    private val distanceTimeTextView by lazy {
        activityMainBinding.distanceTimeTextView
    }

    private val stopsOverlay: GraphicsOverlay by lazy { GraphicsOverlay() }
    private val routeOverlay: GraphicsOverlay by lazy { GraphicsOverlay() }

    private val envelope = Envelope(
        Point(-1.3045e7, 3.87e6, 0.0, SpatialReference.webMercator()),
        Point(-1.3025e7, 3.84e6, 0.0, SpatialReference.webMercator())
    )

    // create a route task to calculate routes
    private var routeTask: RouteTask? = null

    private var routeParameters: RouteParameters? = null

    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)
        // some parts of the API require an Android Context to properly interact with Android system
        // features, such as LocationProvider and application resources
        ArcGISEnvironment.applicationContext = applicationContext
        lifecycle.addObserver(mapView)

        // create a tile cache from the .tpkx file
        val tileCache = TileCache(provisionPath + getString(R.string.tpkx_path))
        val tiledLayer = ArcGISTiledLayer(tileCache)
        // make a basemap with the tiled layer and add it to the mapview as an ArcGISMap
        mapView.map = ArcGISMap(Basemap(tiledLayer))

        // add the graphics overlays to the map view
        mapView.graphicsOverlays.addAll(listOf(routeOverlay, stopsOverlay))

        // create a route task using the geodatabase file
        val geodatabaseFile = File(provisionPath + getString(R.string.geodatabase_path))
        routeTask = RouteTask(geodatabaseFile.path, "Streets_ND")

        // load the route task
        lifecycleScope.launch {
            routeTask?.load()?.onFailure {
                showError(it.message.toString())
            }?.onSuccess {
                // use the default parameters for the route calculation
                routeParameters = routeTask?.createDefaultParameters()?.getOrThrow()
            }
        }

        toggleButtons.addOnButtonCheckedListener { _, checkedId, isChecked ->
            if (isChecked) {
                when (checkedId) {
                    R.id.fastestButton -> {
                        // calculate fastest route
                        routeParameters?.travelMode =
                            routeTask?.getRouteTaskInfo()?.travelModes?.get(0)

                        // update route based on selection
                        updateRoute()
                    }
                    R.id.shortestButton -> {
                        // calculate shortest route
                        routeParameters?.travelMode =
                            routeTask?.getRouteTaskInfo()?.travelModes?.get(1)

                        // update route based on selection
                        updateRoute()
                    }
                }
            }
        }

        // make a clear button to reset the stops and routes
        clearButton.setOnClickListener {
            stopsOverlay.graphics.clear()
            routeOverlay.graphics.clear()
            clearButton.isEnabled = false
            distanceTimeTextView.text = getString(R.string.tap_on_map_to_create_a_transport_network)
        }

        // set up the touch listeners on the map view
        setUpMapView()
    }

    /**
     * Sets up the viewpoint and onSingleTapConfirmed for the mapView.
     * For single taps, graphics will be selected.
     * */
    private fun setUpMapView() {
        with(lifecycleScope) {
            // set the viewpoint of the MapView
            launch {
                mapView.setViewpointGeometry(envelope)
            }

            // add graphic at the tapped coordinate
            launch {
                mapView.onSingleTapConfirmed.collect { tapEvent ->
                    val screenCoordinate = tapEvent.screenCoordinate
                    addOrSelectGraphic(screenCoordinate)
                    clearButton.isEnabled = true
                }
            }
        }
    }

    /**
     * Updates the calculated route using the
     * stops on the map by calling routeTask.solveRoute().
     * Creates a graphic to display the route.
     * */
    private fun updateRoute() = lifecycleScope.launch {
        // get a list of stops from the graphics currently on the graphics overlay.
        val stops = stopsOverlay.graphics.map {
            Stop(it.geometry as Point)
        }

        // do not calculate a route if there is only one stop
        if (stops.size <= 1) return@launch

        routeParameters?.setStops(stops)

        // solve the route
        val results = routeParameters?.let { routeTask?.solveRoute(it) }
        if (results != null) {
            results.onFailure {
                showError("No route solution. ${it.message}")
                routeOverlay.graphics.clear()
            }.onSuccess { routeResult ->
                // get the first solved route result
                val route = routeResult.routes[0]

                // create graphic for route
                val graphic = Graphic(
                    route.routeGeometry, SimpleLineSymbol(
                        SimpleLineSymbolStyle.Solid,
                        Color.black, 3F
                    )
                )
                routeOverlay.graphics.clear()
                routeOverlay.graphics.add(graphic)

                // set distance-time text
                val travelTime = route.travelTime.roundToInt()
                val travelDistance = "%.2f".format(
                    route.totalLength * 0.000621371192 // convert meters to miles and round 2 decimals
                )
                distanceTimeTextView.text = String.format("$travelTime min ($travelDistance mi)")
            }
        }
    }

    /**
     * Selects a graphic if there is one at the
     * provided [screenCoordinate] or, if there is
     * none, creates a new graphic.
     * */
    private suspend fun addOrSelectGraphic(screenCoordinate: ScreenCoordinate) {
        // identify the selected graphic
        val result =
            mapView.identifyGraphicsOverlay(stopsOverlay, screenCoordinate, 10.0, false)

        result.onFailure {
            showError(it.message.toString())
        }.onSuccess { identifyGraphicsOverlayResult ->
            val graphics = identifyGraphicsOverlayResult.graphics

            // unselect everything
            if (stopsOverlay.selectedGraphics.isNotEmpty()) {
                stopsOverlay.unselectGraphics(stopsOverlay.selectedGraphics)
            }

            // if the user tapped on something, select it
            if (graphics.isNotEmpty()) {
                val firstGraphic = graphics[0]
                firstGraphic.isSelected = true
            } else { // there is no graphic at this location
                val locationPoint = mapView.screenToLocation(screenCoordinate)
                // check if tapped location is within the envelope
                if (GeometryEngine.within(locationPoint as Geometry, envelope))
                    // make a new graphic at the tapped location
                    createStopSymbol(stopsOverlay.graphics.size + 1, locationPoint)
                else
                    showError("Tapped location is outside the transport network")
            }
        }
    }

    /**
     * Creates a composite symbol to represent a numbered stop.
     * The [stopNumber] is the ordinal number of this stop and the
     * symbol will be placed at the [locationPoint].
     */
    private fun createStopSymbol(stopNumber: Int, locationPoint: Point?) {
        // create a orange pin PictureMarkerSymbol
        val pinSymbol = PictureMarkerSymbol.createWithImage(
            ContextCompat.getDrawable(
                this,
                R.drawable.pin_symbol
            ) as BitmapDrawable
        ).apply {
            // set the scale of the symbol
            width = 24f
            height = 24f
            // set in pin "drop" to be offset to the point on map
            offsetY = 10f
        }

        // create black stop number TextSymbol
        val stopNumberSymbol = TextSymbol(
            stopNumber.toString(),
            Color.black,
            12f,
            HorizontalAlignment.Center,
            VerticalAlignment.Bottom
        ).apply {
            offsetY = 4f
        }

        // create a composite symbol and add the picture marker symbol and text symbol
        val compositeSymbol = CompositeSymbol()
        compositeSymbol.symbols.addAll(listOf(pinSymbol, stopNumberSymbol))

        // create a graphic to add to the overlay and update the route
        val graphic = Graphic(locationPoint, compositeSymbol)
        stopsOverlay.graphics.add(graphic)

        updateRoute()
    }

    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.