You will learn: how to find the optimal route and directions offline for multiple stops with the ArcGIS Online Network Analytics Route service and StreetMap Premium.
StreetMap Premium provides high-quality data optimized for cartographic map display, geocoding, and routing. StreetMap Premium data is delivered via mobile map packages (.mmpk). These can be used for routing and geocoding while offline. You can use a RouteTask and the SolveRouteAsync
method to find routes. RouteTask
accepts "stop" locations and returns a route with directions. Once you have the results you can add the route to a map, display the turn-by-turn directions, or otherwise integrate them into your application.
In this tutorial you will learn how to use a transportation network from StreetMap Premium to calculate an optimal route between two stops. Stop locations will be selected with clicks on the map.
If you have not done the starter project tutorial, be sure to review it first so you are familiar with running the app.
In a new or empty folder, make a copy of the Create a starter app solution.
Complete or review tutorials Add layers to a map (offline), Create graphics, Display point, line, and polygon graphics, and [Get a route and directions as you will repeat some parts of those tutorials here.
rootProject.name
value to get-a-route-and-directions-offline
.Your app will need to either include the StreetMap Premium .mmpk files, or be able to download them on demand. For this tutorial, the data will be provided, California package. Add this to the root of the project.
Open the App.java file in src/main/java/com/arcgis/developers/labs. Then add the following with the existing imports.
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import javafx.geometry.Point2D;
import javafx.scene.control.Alert;
import javafx.scene.input.MouseButton;
import com.esri.arcgisruntime.concurrent.ListenableFuture;
import com.esri.arcgisruntime.data.TransportationNetworkDataset;
import com.esri.arcgisruntime.geometry.Point;
import com.esri.arcgisruntime.geometry.Polyline;
import com.esri.arcgisruntime.loadable.LoadStatus;
import com.esri.arcgisruntime.mapping.MobileMapPackage;
import com.esri.arcgisruntime.mapping.Viewpoint;
import com.esri.arcgisruntime.mapping.view.Graphic;
import com.esri.arcgisruntime.mapping.view.GraphicsOverlay;
import com.esri.arcgisruntime.symbology.SimpleLineSymbol;
import com.esri.arcgisruntime.symbology.SimpleMarkerSymbol;
import com.esri.arcgisruntime.tasks.networkanalysis.Route;
import com.esri.arcgisruntime.tasks.networkanalysis.RouteParameters;
import com.esri.arcgisruntime.tasks.networkanalysis.RouteResult;
import com.esri.arcgisruntime.tasks.networkanalysis.RouteTask;
import com.esri.arcgisruntime.tasks.networkanalysis.Stop;
Add new private fields that will be used later.
/* ** ADD ** */
private Point startPoint;
private Point endPoint;
private final int hexRed = 0xFFFF0000;
private final int hexBlue = 0xFF0000FF;
private GraphicsOverlay graphicsOverlay;
private RouteTask solveRouteTask;
private RouteParameters routeParameters;
private TransportationNetworkDataset transportationNetwork;
/* ** ADD ** */
private MapView mapView;
Add a new private method setupGraphicsOverlay
that creates a graphics overlay and attaches it to the map view to render the graphics to the map. Each graphic must have a symbol assigned in order for it to be visible on the map.
private void setupGraphicsOverlay() {
if (mapView != null) {
graphicsOverlay = new GraphicsOverlay();
mapView.getGraphicsOverlays().add(graphicsOverlay);
}
}
Make a call to the method just created in the start method.
setupMap();
/* ** ADD ** */
setupGraphicsOverlay();
Add a new private method setupMobileMap
that loads a mmpk file into a map object and then updates the map view to display it. The
map package is loaded asynchronously so it does not slow the UI. If the map package fails to load for any reason, we fallback to the
online map we had before.
private void setupMobileMap() {
if (mapView != null) {
String mmpkFile = "./California.mmpk";
final MobileMapPackage mapPackage = new MobileMapPackage(mmpkFile);
mapPackage.addDoneLoadingListener(() -> {
if (mapPackage.getLoadStatus() == LoadStatus.LOADED && mapPackage.getMaps().size() > 0) {
} else {
setupMap();
new Alert(Alert.AlertType.ERROR, "MMPK failed to load: " + mapPackage.getLoadError().getMessage())
.show();
}
});
mapPackage.loadAsync();
}
}
Set map from map package to the map view and get TransportationNetworkDataset
from that map.
if (mapPackage.getLoadStatus() == LoadStatus.LOADED && mapPackage.getMaps().size() > 0) {
/* ** ADD ** */
double latitude = 34.05293;
double longitude = -118.24368;
double scale = 220000;
ArcGISMap map = mapPackage.getMaps().get(0);
transportationNetwork = map.getTransportationNetworks().get(0);
mapView.setMap(map);
map.setInitialViewpoint(new Viewpoint(latitude, longitude, scale));
//setupRouteTask();
Look for these lines of the code in the start method
mapView.setMap(map);
setupMap();
and replace setupMap()
with setupMobileMap()
:
mapView.setMap(map);
setupMobileMap();
Initialize and load the route task using the transportation network. The route task will calculate a route between two points using the detailed information transportation network.
private void setupRouteTask() {
solveRouteTask = new RouteTask(transportationNetwork);
solveRouteTask.loadAsync();
}
Once the route task has loaded create default route parameters.
solveRouteTask.loadAsync();
/* ** ADD ** */
solveRouteTask.addDoneLoadingListener(() -> {
if (solveRouteTask.getLoadStatus() == LoadStatus.LOADED) {
final ListenableFuture<RouteParameters> routeParamsFuture = solveRouteTask.createDefaultParametersAsync();
routeParamsFuture.addDoneListener(() -> {
try {
routeParameters = routeParamsFuture.get();
//createRouteAndDisplay();
} catch (InterruptedException | ExecutionException e) {
new Alert(Alert.AlertType.ERROR, "Cannot create RouteTask parameters " + e.getMessage()).show();
}
});
} else {
new Alert(Alert.AlertType.ERROR, "Unable to load RouteTask " + solveRouteTask.getLoadStatus().toString()).show();
}
});
Uncomment setupRouteTask
from setupMobileMap
method.
map.setInitialViewpoint(new Viewpoint(latitude, longitude, scale));
setupRouteTask();
Add a private method setMapMarker
that creates a graphic at a specified location using a symbol and display it to the map.
private void setMapMarker(Point location, SimpleMarkerSymbol.Style style, int markerColor, int outlineColor) {
final float markerSize = 8;
final float markerOutlineThickness = 2;
SimpleMarkerSymbol pointSymbol = new SimpleMarkerSymbol(style, markerColor, markerSize);
pointSymbol.setOutline(new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, outlineColor, markerOutlineThickness));
Graphic pointGraphic = new Graphic(location, pointSymbol);
graphicsOverlay.getGraphics().add(pointGraphic);
}
Add a new private method setStartMarker
that visually displays and stores the starting location.
private void setStartMarker(Point location) {
graphicsOverlay.getGraphics().clear();
setMapMarker(location, SimpleMarkerSymbol.Style.DIAMOND, hexRed, hexBlue);
startPoint = location;
endPoint = null;
}
Add a new private method setEndMarker
that visually displays and stores the ending location.
private void setEndMarker(Point location) {
setMapMarker(location, SimpleMarkerSymbol.Style.SQUARE, hexBlue, hexRed);
endPoint = location;
// solveForRoute();
}
Add a private method createRouteAndDisplay
that adds a listener to the map view for interpreting user input when the map is clicked. Then call the graphic display methods you coded in the prior step.
private void createRouteAndDisplay() {
mapView.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY && e.isStillSincePress()) {
Point2D point = new Point2D(e.getX(), e.getY());
Point mapPoint = mapView.screenToLocation(point);
if (startPoint == null) {
setStartMarker(mapPoint);
} else if (endPoint == null) {
setEndMarker(mapPoint);
} else {
setStartMarker(mapPoint);
}
}
});
}
Uncomment createRouteAndDisplay
from setupRouteTask
method.
routeParameters = routeParamsFuture.get();
createRouteAndDisplay();
Add a private method solveForRoute
that solves for a route between two locations and displays that route on the map.
private void solveForRoute() {
if (startPoint != null && endPoint != null) {
routeParameters.setStops(Arrays.asList(new Stop(startPoint), new Stop(endPoint)));
final ListenableFuture<RouteResult> routeResultFuture = solveRouteTask.solveRouteAsync(routeParameters);
}
}
Wait for task to be complete before accessing the results.
final ListenableFuture<RouteResult> routeResultFuture = solveRouteTask.solveRouteAsync(routeParameters);
/* ** ADD ** */
routeResultFuture.addDoneListener(() -> {
try {
RouteResult routeResult = routeResultFuture.get();
} catch (InterruptedException | ExecutionException e) {
new Alert(Alert.AlertType.ERROR, "Solve RouteTask failed " + e.getMessage() + e.getMessage()).show();
}
});
Check that a route was returned and selected the first one. Display this route to the map using the graphics overlay.
RouteResult routeResult = routeResultFuture.get();
/* ** ADD ** */
if(routeResult.getRoutes().size() > 0) {
Route firstRoute = routeResult.getRoutes().get(0);
Polyline routePolyline = firstRoute.getRouteGeometry();
SimpleLineSymbol routeSymbol = new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, hexBlue, 4.0f);
Graphic routeGraphic = new Graphic(routePolyline, routeSymbol);
graphicsOverlay.getGraphics().add(routeGraphic);
} else {
new Alert(Alert.AlertType.WARNING, "No routes have been found.").show();
}
Uncomment the call to solveForRoute()
in the setEndMarker
method.
endPoint = location;
solveForRoute();
Your map displays a route between two clicked locations. Compare with the completed solution project.
The RouteParameters
class offers a lot of options. Explore some of these options and enhance your app. Can you show turn-by-turn directions?
The solution route, called firstRoute in this example, has additional information accessible through a series of properties. Enhance your app by displaying additional information about the route solution, such as travel time and route length.
long lengthInKm = Math.round(firstRoute.getTotalLength() / 1000.0);
long timeInMinutes = Math.round(firstRoute.getTravelTime());
new Alert(Alert.AlertType.INFORMATION, "Total length (km): " + lengthInKm + " Travel time (min): " + timeInMinutes).show();
Combine what you learned in Search for an address to allow your user to search for the start and end locations.
In this tutorial you handled the first result but ignored any additional results. Can you create a UI to allow the user to review multiple results?