9. Preview Route

The following tutorial will demonstrate how to request a route using the UNL SDK and preview it on the map. It will exemplify the request of both: outdoor-to-outdoor and outdoor-to-indoor routes.

First, lets start by adding this new option in the ActionSheet.js file:

<button id="preview-route-button" class="action-sheet-button">
Preview route
</button>

Next, we need to initialize the mapbox source and layers for the route polyline:

export const renderRoute = (map) => {
map.addSource("route", {
type: "geojson",
data: {
type: "Feature",
geometry: {
type: "LineString",
coordinates: [],
},
},
});
map.addLayer(
{
id: "route",
type: "line",
source: "route",
paint: {
"line-color": "#32C5FF",
"line-width": 5,
},
},
"routeDestinationMarker"
);
};

Same for the two markers that highlight the starting and the ending point of the route. The coordinates of the starting point were hardcoded for the purpose of this tutorial:

const ROUTE_STARTING_POINT = [13.38010311126709, 52.5201416015625];
export const renderRouteSourceMarker = (map) => {
map.addSource("routeSourceMarker", {
type: "geojson",
data: {
type: "Feature",
geometry: {
type: "Point",
coordinates: ROUTE_STARTING_POINT,
},
},
});
map.addLayer({
id: "routeSourceMarker",
type: "symbol",
source: "routeSourceMarker",
layout: {
"icon-image": "route_source_icon",
"icon-size": 0.7,
"icon-allow-overlap": true,
},
});
};
export const renderRouteDestinationMarker = (map) => {
map.addSource("routeDestinationMarker", {
type: "geojson",
data: {
type: "Feature",
geometry: {
type: "Point",
coordinates: [],
},
},
});
map.addLayer({
id: "routeDestinationMarker",
type: "symbol",
source: "routeDestinationMarker",
layout: {
"icon-image": "route_destination_icon",
"icon-size": 0.5,
"icon-offset": [0, -40],
"text-font": ["Fira GO Regular"],
"icon-allow-overlap": true,
},
});
};

Now that we have the visual elements in place, lets proceed with requesting the actual route. We have to add a new function in the unlApi.js file which calls the route method of the sdk:

export const fetchRoute = (routeRequest) => {
return unlApi.routingApi.route(routeRequest);
};

Next, lets implement the function which builds the routeRequest object that will be passed as a parameter to the function above. The routeRequest object contains an array of waypoints. The first element of the array contains the coordinates of the starting point. For the second element, which represents the destination, a function named buildDestinationWaypoint is called. This will determine whether the point is an indoor or an outdoor location and build the object accordingly:

export const previewRoute = async (map) => {
const destinationMapSource = map.getSource("unlCell");
const destinationCell = destinationMapSource._data.geometry.coordinates;
if (!destinationCell.length) {
alert("Select the destination cell!");
} else {
const sourceCoordinates =
map.getSource("routeSourceMarker")._data.geometry.coordinates;
const destinationCellCorner = destinationCell[0][1];
const destinationGeohash = UnlCore.encode(
destinationCellCorner[1],
destinationCellCorner[0],
9 // geohash precision
);
const routeRequest = {
preference: "fastest",
waypoints: [
{
type: "point",
coordinates: sourceCoordinates[0] + "," + sourceCoordinates[1],
},
{
...buildDestinationWaypoint(destinationMapSource, destinationGeohash),
},
],
};
const route = await fetchRoute(routeRequest);
const routeCoordinates = route.overview.linestring.map((geohash) => {
const decodedGeohash = UnlCore.decode(geohash);
const lng = decodedGeohash.lon;
const lat = decodedGeohash.lat;
const elevation = decodedGeohash.elevation;
return { coordinates: [Number(lng), Number(lat)], elevation };
});
const elevatedRoute = groupBy(routeCoordinates, "elevation");
const routeSegments = [];
Object.keys(elevatedRoute).map((key) => {
return routeSegments.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: elevatedRoute[key],
},
properties: {
elevation: Number(key),
},
});
});
map
.getSource("route")
.setData({ type: "FeatureCollection", features: routeSegments });
if (routeSegments.length < 2) {
map.setFilter("route", ["==", "elevation", 0]);
}
map.setLayoutProperty("routeSourceMarker", "visibility", "visible");
const destinationCoordinates = geohashToCoordinates(destinationGeohash);
updateDestinationMarkerPosition(map, destinationCoordinates);
}
};

Below is the buildDestinationWaypoint function:

const buildDestinationWaypoint = (destinationMapSource, destinationGeohash) => {
const destinationSourceProperties = destinationMapSource._data.properties;
const projectId = config.PROJECT_ID;
if (destinationSourceProperties.venueId) {
// indoor waypoint
const indoorWaypoint = {
type: "indoor",
venueId: destinationSourceProperties.venueId,
unitId: destinationSourceProperties.id,
projectId: projectId,
};
return indoorWaypoint;
} else {
// outdoor waypoint
return {
type: "geohash",
geohash: destinationGeohash,
};
}
};

Add a new filter for the level selector to show only the parts of the route that correspond to the selected venue level in case of an indoor route in the handleLevelSelected function.

const routeFilter = ["==", "elevation", levelIndex];
map.setFilter("route", routeFilter);
if (map.getSource("route")._data.features.length < 2) {
map.setFilter("route", ["==", "elevation", 0]);
}

Finally, add the click event listener to the menu option and call the previewRoute function:

document
.getElementById("preview-route-button")
.addEventListener("click", () => {
previewRoute(map);
});

Let's see how it looks: