How to build an online route planner with Amazon Location Service
Tags: vue, aws, geospatial, amazonlocationservice
Published: 2022-09-14 05:59:18

Route finding is something that many of us use almost everyday to go from one place to another. Depending on our daily routines, personal preferences, or occupation, we may rely on different modes of transportation, choose different times of day, or consider some restrictions (or avoidances) to get to our destination(s). We usually use an app for this purpose and there are many of them out there. In this post, you will learn how to build one yourself on AWS in seven steps.

We will build a [Vue](https://vuejs.org/) app using [Amazon Location Service](http://%20https//aws.amazon.com/location/?trk=c5799a1c-5743-48da-8900-0b1392ff83c0&sc_channel=el), as the basemap provider and routing API, [MapLibre GL JS](https://maplibre.org/), as the map rendering library, and [Naive UI](https://www.naiveui.com/en-US/os-theme), as the UI component library. The app will have a map with navigation controls and a route calculator that finds the fastest route using a number of parameters such as different modes of transportation (car, truck, and walking), departure time, avoidances (ferries and tolls), and weight and size limitations for trucks — the following screenshot shows the end results.


![An online route planner](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b81b9f9fkrzk3tnikqxm.png)

## Prerequisites

We will create the AWS resources for this project using [AWS CLI](https://aws.amazon.com/cli/). Before we start

* [Create an AWS account](https://portal.aws.amazon.com/billing/signup?trk=6c00012e-f2a0-477d-9f27-c077afd6b3ba&sc_channel=el), if you do not have one already.
* [Install](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) AWS CLI on your machine.

## 1. Create a map resource

[Amazon Location Service’s Maps API](https://docs.aws.amazon.com/location/latest/developerguide/map-concepts.html?trk=df0383ab-72a2-4da2-9f19-9ebd7a75e919&sc_channel=el) offers a set of map styles, professionally designed to support different applications and use cases — see [Esri map styles](https://docs.aws.amazon.com/location/latest/developerguide/esri.html?trk=f04d2a1d-a50e-4ae6-8fbc-1f531b69b8c9&sc_channel=el) and [HERE map styles](https://docs.aws.amazon.com/location/latest/developerguide/HERE.html?trk=148f3bde-e031-44a0-9f0e-5986dab5b436&sc_channel=el) for more details. Now, create a map resource using the map style `Esri Navigation`.

```bash
aws location create-map \
--map-name=esri-map-navigation \
--configuration Style=VectorEsriNavigation \
--profile={YOUR_AWS_PROFILE}
```

Once a new map resource is created, you will see an output that contains the map resource’s creation time, ARN, and name.

```json
{
    "CreateTime": "2022-09-03T21:00:41.391000+00:00",
    "MapArn": "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT}:map/esri-map-navigation",
    "MapName": "esri-map-navigation"
}
```

## 2. Create a route calculator resource

[Amazon Location Service’s Routes API](https://docs.aws.amazon.com/location/latest/developerguide/route-concepts.html?trk=4ed3e774-48de-4772-81e3-a0f777f13d52&sc_channel=el) provides a route calculator resource that finds optimal routes based on up-to-date road networks and traffic information from two global data providers, Esri and HERE. Let’s create a route calculator resource, with Esri as our data provider.

```bash
aws location create-route-calculator \
--calculator-name "esri-route-calculator" \
--data-source "Esri" \
--profile={YOUR_AWS_PROFILE}
```

Once a new route calculator resource is created, you will see an output that contains the route calculator resource’s creation time, ARN, and name.

```json
{
    "CreateTime": "2022-09-03T21:25:36.297000+00:00",
    "CalculatorArn": "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT}:route-calculator/esri-route-calculator",
    "CalculatorName": "esri-route-calculator"
}
```

## 3. Grant access to Amazon Location resources

Now, you need to grant access to the resources that you have created so far by creating an [Amazon Cognito](https://aws.amazon.com/cognito/) identity pool and an IAM policy. This way a frontend application can send signed HTTP requests to Amazon Cognito and receive temporary, scoped-down credentials that are valid for an hour. Then, it can use those credentials to request map tiles and optimal routes from Amazon Location Service’s Maps and Routes APIs.

For this project, we will allow for unauthenticated guest access to our application — see [Granting access to Amazon Location Service](https://docs.aws.amazon.com/location/latest/developerguide/how-to-access.html?trk=ecef015a-6304-4acf-b3a2-37f69396b937&sc_channel=el) to explore more options. First, create an Amazon Cognito identity pool.

```shell
aws cognito-identity create-identity-pool \
--identity-pool-name routing-app \
--allow-unauthenticated-identities \
--profile={YOUR_AWS_PROFILE}
```

Once a new identity pool is created, you will see an output that contains the identity pool ID, name, and a confirmation for allowing unauthenticated access.

```json
{
    "IdentityPoolId": "{IDENTITY_POOL_ID}",
    "IdentityPoolName": "routing-app",
    "AllowUnauthenticatedIdentities": true,
    "IdentityPoolTags": {}
}
```

Next, create a new IAM role that you want to use with your identity pool. Note that you must provide a policy document, as an input parameter, to establish trust between Amazon Cognito and [AWS Security Token Service (STS)](https://docs.aws.amazon.com/iam/index.html#sts). This will allow your identity pool to request temporary tokens from STS. Here is an example of a policy document in JSON — make sure to convert it to string before using it with the CLI command.

```json
{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Effect":"Allow",
         "Principal":{
            "Federated":"cognito-identity.amazonaws.com"
         },
         "Action":"sts:AssumeRoleWithWebIdentity",
         "Condition":{
            "StringEquals":{
               "cognito-identity.amazonaws.com:aud":"{IDENTITY_POOL_ID}"
            },
            "ForAnyValue:StringLike":{
               "cognito-identity.amazonaws.com:amr":"unauthenticated"
            }
         }
      }
   ]
}
```

Now, let’s create a new IAM role.

```bash
aws iam create-role \
--role-name Cognito_routing_app_Unauth_Role \
--assume-role-policy-document "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Federated\":\"cognito-identity.amazonaws.com\"},\"Action\":\"sts:AssumeRoleWithWebIdentity\",\"Condition\":{\"StringEquals\":{\"cognito-identity.amazonaws.com:aud\":\"{IDENTITY_POOL_ID}\"},\"ForAnyValue:StringLike\":{\"cognito-identity.amazonaws.com:amr\":\"unauthenticated\"}}}]}" \
--profile={YOUR_AWS_PROFILE}
```

Once a new IAM role is created, you will see an output that contains some metadata about the role including the role name and the policy document you provided in the request.

```json
{
    "Role": {
        "Path": "/",
        "RoleName": "Cognito_routing_app_Unauth_Role",
        "RoleId": "AROA3K4ALINNDDUQ3Q2XB",
        "Arn": "arn:aws:iam::{AWS_ACCOUNT}:role/Cognito_routing_app_Unauth_Role",
        "CreateDate": "2022-09-03T22:19:56+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Federated": "cognito-identity.amazonaws.com"
                    },
                    "Action": "sts:AssumeRoleWithWebIdentity",
                    "Condition": {
                        "StringEquals": {
                            "cognito-identity.amazonaws.com:aud": "{IDENTITY_POOL_ID}"
                        },
                        "ForAnyValue:StringLike": {
                            "cognito-identity.amazonaws.com:amr": "unauthenticated"
                        }
                    }
                }
            ]
        }
    }
}
```

Then, attach an inline policy document to the IAM role that you have just created. The policy will grant access to your map and route calculator resources. Here is an example of a policy document in JSON — make sure to convert it to string before using it with the CLI command.

```json
{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Sid":"MapsRoutesReadOnly",
         "Effect":"Allow",
         "Action":[
            "geo:GetMap*",
            "geo:CalculateRoute"
         ],
         "Resource":[
            "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT}:map/esri-map-navigation",
            "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT}:route-calculator/esri-route-calculator"
         ]
      }
   ]
}
```

Now, let’s attach the policy to the IAM role.

```bash
aws iam put-role-policy \
--role-name Cognito_routing_app_Unauth_Role \
--policy-name Cognito_routing_app_Unauth_Role_Policy \
--policy-document "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"MapsRoutesReadOnly\",\"Effect\":\"Allow\",\"Action\":[\"geo:GetMap*\",\"geo:CalculateRoute\"],\"Resource\":[\"arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT}:map/esri-map-navigation\",\"arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT}:route-calculator/esri-route-calculator\"]}]}" \
--profile={YOUR_AWS_PROFILE}
```

Finally, add the IAM role to your identity pool.

```bash
aws cognito-identity set-identity-pool-roles \
--identity-pool-id "{IDENTITY_POOL_ID}" \
--roles unauthenticated="arn:aws:iam::{AWS_ACCOUNT}:role/Cognito_routing_app_Unauth_Role" \
--profile={YOUR_AWS_PROFILE}
```

## 4. Set up a new project

Create a new Vue project using [Vite](https://vitejs.dev/).

```bash
npm create vite@latest
```

Choose a name for your project and pick `vue` as the framework and variant.

```bash
✔ Select a variant: › vue
✔ Project name: amazon-location-route-planner
✔ Select a framework: › vue
```

Next, go into your project’s root directory and install the dependencies.

```bash
cd amazon-location-route-planner
npm install
```

Then, replace the content of `App.vue` with the following code.

```vue
<template>
</template>

<script setup>
</script>

<style>
</style>
```

Finally, add the following configuration to `vite.config`.

```javascript
export default defineConfig({
  ...

  define: {
    global: {}
  }
});
```

## 5. Add auth configurations

First, install a few dependencies.

```bash
npm install @aws-amplify/core @aws-sdk/client-cognito-identity @aws-sdk/credential-provider-cognito-identity
```

Next, create a new file, `config.js`, under `src` directory and enter your Cognito identity pool ID.

```javascript
const identityPoolId = "{IDENTITY_POOL_ID}";

export { identityPoolId };
```

Then, create a new file, `auth.js`, under `src` directory and add the following code to the new file. This will provide a few variables and functions that we will use later on to communicate with Amazon Location Service’s APIs: 

* `region` which indicates your AWS region
* `credentials`, which are the credentials obtained from Amazon Cognito
* `refreshCredentials`, which is a function that automatically renews credentials before they expire
* `transformRequest`, which is a function that signs HTTP request sent to the Amazon Location Service’s Maps API using AWS SigV4 with the credentials obtained from Amazon Cognito

```javascript
import { Signer } from "@aws-amplify/core";
import { fromCognitoIdentityPool } from "@aws-sdk/credential-provider-cognito-identity";
import { CognitoIdentityClient } from "@aws-sdk/client-cognito-identity";
import { identityPoolId } from "./config";

const region = identityPoolId.split(":")[0];

const identityProvider = fromCognitoIdentityPool({
  client: new CognitoIdentityClient({
    region: region,
  }),
  identityPoolId,
});

let credentials;
const refreshCredentials = async () => {
  credentials = await identityProvider();
  setTimeout(refreshCredentials, credentials.expiration - new Date());
};

const transformRequest = (url, resourceType) => {
  if (resourceType === "Style" && !url.includes("://")) {
    url = `https://maps.geo.${region}.amazonaws.com/maps/v0/maps/${url}/style-descriptor`;
  }

  if (url.includes("amazonaws.com")) {
    return {
      url: Signer.signUrl(url, {
        access_key: credentials.accessKeyId,
        secret_key: credentials.secretAccessKey,
        session_token: credentials.sessionToken,
      }),
    };
  }

  return { url };
};

export { region, credentials, refreshCredentials, transformRequest };
```

## 6. Add a map

First, install `maplibre-gl`, our map rendering library.

```bash
npm install maplibre-gl
```

Next, open `src/config.js` and add the name of your map resource.

```javascript
...

const mapName = "esri-map-navigation";

export { identityPoolId, mapName };
```

Then, open `App.vue` and add a `div` container for the map.

```vue
<template>
   <div id="map"></div>
</template>
```

Afterwards, add a new map instance with navigation controls.

```vue
<script setup>
import maplibregl from "maplibre-gl";
import { refreshCredentials, transformRequest } from "./auth";
import { mapName } from "./config";

let map = null;

const initializeApp = async () => {
  await refreshCredentials();

  map = new maplibregl.Map({
    container: "map",
    center: [-114.067375, 51.046333],
    zoom: 16,
    style: mapName,
    hash: true,
    transformRequest,
  });

  map.addControl(new maplibregl.NavigationControl(), "bottom-right");
}

initializeApp();
</script>
```

Finally, add the following CSS to produce a full screen map.

```vue
<style>
@import "maplibre-gl/dist/maplibre-gl.css";

body {
  margin: 0;
  padding: 0;
}
#map {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 100%;
}
</style>
```

Now, if you run `npm run dev` in your terminal and go to your browser, you will see an interactive map on the page.

## 7. Add a route calculator

An Amazon Location Service’s route calculator resource provides an operation called `CalculateRoute` that finds optimal routes between an origin, a destination, and up to 23 stops along the way (or waypoints) for different modes of transportation, avoidances, and departure times — check out the [API docs](https://docs.aws.amazon.com/location/latest/APIReference/welcome.html?trk=f2ac84bd-060c-45b2-b656-2be56f04965d&sc_channel=el) to see all the supported parameters.

For this project, we will build a route calculator widget that supports the following parameters: 

* `DeparturePosition` — It specifies the starting point as `[longitude, latitude]`.
* `DestinationPosition` — It specifies the destination as `[longitude, latitude]`.
* `WaypointPositions` — It specifies an ordered list of up to 23 intermediate positions to include along a route between the departure and destination positions as `[[longitude, latitude], [longitude, latitude]]`.
* `TravelMode` — It specifies the mode of transportation when calculating a route, including `Car`, `Truck`, or `Walking`.
* `DistanceUnit` — It specifies the unit of measurement for travel distance, including `Kilometers` or `Miles`.
* `CarModeOptions` — It specifies route preferences when traveling by Car, including avoiding routes that use ferries , `AvoidFerries`, and tolls, `AvoidTolls`.
* `TruckModeOptions` — It specifies route preferences when traveling by Truck, including avoiding routes that use ferries, `AvoidFerries`, and tolls, `AvoidTolls`. It also specifies the weight, `Weight`, and size, including `Height`, `Length`, and `Width`, of the truck to avoid routes that will not accommodate it.
* `DepartNow` — It sets the time of departure as the current time.
* `DepartureTime` — It specifies the desired time of departure.

The calculated route will include the overall travel time and distance, unit of measurement, and route geometry, among other things — check out the [API docs](https://docs.aws.amazon.com/location/latest/APIReference/welcome.html?trk=f2ac84bd-060c-45b2-b656-2be56f04965d&sc_channel=el) to see the API response elements.

Now, let’s start with installing a few dependencies.

```bash
npm install @aws-sdk/client-location @turf/helpers

npm install -D naive-ui vfonts @vicons/carbon
```

Next, open `src/config.js` and add the name of your route calculator resource.

```javascript
...

const routeCalculatorName = "esri-route-calculator";

export { identityPoolId, mapName, routeCalculatorName };
```

Afterwards, open `App.vue` and add a card with seven sections for (1) transportation mode, (2) route summary, (3) starting point, waypoints, and destination, (4) unit of measurement options, (5) avoid options, (6) departure time options, and (7) truck options. Note that the last three sections are only for cars and trucks.

```vue
<template>
  ...
  
  <n-card>
    <n-space vertical>
      
      <!-- Travel mode options -->
      <n-radio-group v-model:value="travelModeOptions">
        <n-radio-button value="Car"> Car </n-radio-button>
        <n-radio-button value="Truck"> Truck </n-radio-button>
        <n-radio-button value="Walking"> Walking </n-radio-button>
      </n-radio-group>
      
      <n-space style="margin: 20px 0 20px 0">
        Click on the map to choose your starting point and destination(s).
      </n-space>
      
      <!-- Route summary -->
      <n-h3 v-if="route.features.length > 0">
        <n-text type="primary">
          {{ Math.round(route.features[0].properties.DurationSeconds / 60) }}
          Minutes
        </n-text>
        <n-text depth="3">
          ({{ route.features[0].properties.Distance.toFixed(1) }}
          {{ route.features[0].properties.DistanceUnit }})
        </n-text>
      </n-h3>
      
      <!-- Starting point and destinations -->
      <n-space>
        <n-timeline>
          <n-timeline-item
            v-for="position in positions.features"
            type="info"
            :content="position.geometry.coordinates.join(', ')"
            :title="position.properties.title"
            line-type="dashed"
          />
        </n-timeline>
      </n-space>
    </n-space>
    <n-collapse style="margin-top: 20px">
      
      <!-- Unit of measurement options -->
      <n-collapse-item title="Unit of measurement" name="1">
        <n-radio-group v-model:value="unitOfMeasurementOptions">
          <div>
            <n-radio value="metric"> Metric </n-radio>
          </div>
          <div>
            <n-radio value="imperial"> Imperial </n-radio>
          </div>
        </n-radio-group>
        <template #header-extra>
          <n-icon :component="RulerAlt" />
        </template>
      </n-collapse-item>
      
      <!-- Avoid options -->
      <n-collapse-item
        v-if="travelModeOptions === 'Car' || travelModeOptions === 'Truck'"
        title="Avoid options"
        name="2"
      >
        <n-checkbox-group v-model:value="avoidOptions">
          <div>
            <n-checkbox value="AvoidTolls" label="Tolls" />
          </div>
          <div>
            <n-checkbox value="AvoidFerries" label="Ferries" />
          </div>
        </n-checkbox-group>
        <template #header-extra>
          <n-icon :component="DirectionFork" />
        </template>
      </n-collapse-item>
      
      <!-- Truck options -->
      <n-collapse-item
        v-if="travelModeOptions === 'Truck'"
        title="Truck options"
        name="3"
      >
        <n-input
          clearable
          :placeholder="`Height (${units[unitOfMeasurementOptions]['truck']['dimension']})`"
          v-model:value="truckHeight"
        >
          <template #prefix>
            <n-icon :component="FitToHeight" />
          </template>
        </n-input>
        <n-input
          clearable
          :placeholder="`Width (${units[unitOfMeasurementOptions]['truck']['dimension']})`"
          v-model:value="truckWidth"
          style="margin-top: 10px"
        >
          <template #prefix>
            <n-icon :component="CenterToFit" />
          </template>
        </n-input>
        <n-input
          clearable
          :placeholder="`Length (${units[unitOfMeasurementOptions]['truck']['dimension']})`"
          v-model:value="truckLength"
          style="margin-top: 10px"
        >
          <template #prefix>
            <n-icon :component="FitToWidth" />
          </template>
        </n-input>
        <n-input
          clearable
          :placeholder="`Weight (${units[unitOfMeasurementOptions]['truck']['weight']})`"
          v-model:value="truckWeight"
          style="margin-top: 10px"
        >
          <template #prefix>
            <n-icon :component="Scales" />
          </template>
        </n-input>
        <template #header-extra>
          <n-icon :component="DeliveryTruck" />
        </template>
      </n-collapse-item>
      
      <!-- Departure time options -->
      <n-collapse-item
        v-if="travelModeOptions === 'Car' || travelModeOptions === 'Truck'"
        title="Departure time"
        name="4"
      >
        <n-radio-group v-model:value="departureTimeOptions">
          <div>
            <n-radio value="default"> Optimal traffic conditions </n-radio>
          </div>
          <div>
            <n-radio value="now"> Now </n-radio>
          </div>
          <div>
            <n-radio value="custom"> Leave at </n-radio>
          </div>
        </n-radio-group>
        <n-input-group
          v-if="departureTimeOptions === 'custom'"
          style="margin-top: 15px"
        >
          <n-date-picker v-model:value="timestamp" type="datetime" clearable />
        </n-input-group>
        <template #header-extra> <n-icon :component="Time" /> </template>
      </n-collapse-item>
    </n-collapse>
  </n-card>
</template>
```

Next, import the required configurations, functions, and components into `App.vue`.

```vue
<script setup>
...

mport { ref, watch, computed, reactive } from "vue";
import {
  LocationClient,
  CalculateRouteCommand,
} from "@aws-sdk/client-location";
import {
  NCard,
  NRadioGroup,
  NRadioButton,
  NRadio,
  NCheckboxGroup,
  NCheckbox,
  NInput,
  NInputGroup,
  NDatePicker,
  NIcon,
  NSpace,
  NCollapse,
  NCollapseItem,
  NTimeline,
  NTimelineItem,
  NH3,
  NText,
} from "naive-ui";
import {
  RulerAlt,
  DirectionFork,
  Time,
  DeliveryTruck,
  FitToHeight,
  FitToWidth,
  CenterToFit,
  Scales,
} from "@vicons/carbon";
import { point, lineString, featureCollection } from "@turf/helpers";
import {
  region,
  credentials,
  refreshCredentials,
  transformRequest,
} from "./auth";
import { mapName, routeCalculatorName } from "./config";

...
</script>
```

Then, define the required variables to capture the transportation mode, positions (departure, waypoints, and destination), calculated route, unit of measurement, avoid option, departure time option, and the weight and size of trucks.

```vue
<script setup>
...

let travelModeOptions = ref("Car");
const positions = reactive(featureCollection([]));
const route = reactive(featureCollection([]));

let unitOfMeasurementOptions = ref("metric");
const units = {
  metric: {
    route: { distance: "Kilometers" },
    truck: { dimension: "Meters", weight: "Kilograms" },
  },
  imperial: {
    route: { distance: "Miles" },
    truck: { dimension: "Feet", weight: "Pounds" },
  },
};

const avoidOptions = ref([]);
let departureTimeOptions = ref("default");
let timestamp = ref(Date.now());

let truckHeight = ref(null);
let truckWidth = ref(null);
let truckLength = ref(null);
let truckWeight = ref(null);

...
</script>
```

Afterwards, add an event handler to capture clicks on the map and store them in `positions` as GeoJSON.

```vue
<script setup>
...

const initializeApp = async () => {
  ...

  let counter = 0;
  map.on("click", async (e) => {
    counter++;
    const { lngLat } = e;
    const p = point([lngLat.lng.toFixed(5), lngLat.lat.toFixed(5)], {
      title: `Point ${counter}`,
    });
    positions.features.push(p);
  });
}

...
</script>
```

Next, add a computed property to build the request body for the API call.

```vue
<script setup>
...

const requestParams = computed(() => {
  const params = {
    CalculatorName: routeCalculatorName,
    TravelMode: travelModeOptions.value,
    DistanceUnit: units[unitOfMeasurementOptions.value]["route"]["distance"],
    IncludeLegGeometry: true,
  };

  // Positions (DeparturePosition, WaypointPositions, and DestinationPosition)
  if (positions.features.length > 1) {
    params.DeparturePosition = positions.features[0].geometry.coordinates;

    params.DestinationPosition =
      positions.features[positions.features.length - 1].geometry.coordinates;

    params.WaypointPositions = positions.features
      .slice(1, positions.features.length - 1)
      .map((feature) => feature.geometry.coordinates);
  }

  // DepartureTimeOptions
  if (departureTimeOptions.value === "default") {
    delete params.DepartNow;
    delete params.DepartureTime;
  }

  if (departureTimeOptions.value === "now") {
    delete params.DepartureTime;
    params.DepartNow = true;
  }

  if (departureTimeOptions.value === "custom") {
    delete params.DepartNow;
    params.DepartureTime = new Date(timestamp.value);
  }

  // CarModeOptions
  if (travelModeOptions.value === "Car") {
    params.CarModeOptions = {
      ...params.CarModeOptions,
      AvoidFerries:
        avoidOptions.value.findIndex((option) => option === "AvoidFerries") >
        -1,
    };
    params.CarModeOptions = {
      ...params.CarModeOptions,
      AvoidTolls:
        avoidOptions.value.findIndex((option) => option === "AvoidTolls") > -1,
    };
  }

  // TruckModeOptions
  if (travelModeOptions.value === "Truck") {
    params.TruckModeOptions = {
      ...params.TruckModeOptions,
      AvoidFerries:
        avoidOptions.value.findIndex((option) => option === "AvoidFerries") >
        -1,
    };
    params.TruckModeOptions = {
      ...params.TruckModeOptions,
      AvoidTolls:
        avoidOptions.value.findIndex((option) => option === "AvoidTolls") > -1,
    };

    if (
      truckHeight.value != null ||
      truckLength.value != null ||
      truckWidth.value != null
    ) {
      params.TruckModeOptions.Dimensions = {
        Height: truckHeight.value,
        Length: truckLength.value,
        Width: truckWidth.value,
        Unit: units[unitOfMeasurementOptions.value]["truck"]["dimension"],
      };
    }

    if (truckWeight.value != null) {
      params.TruckModeOptions.Weight = {
        Total: truckWeight.value,
        Unit: units[unitOfMeasurementOptions.value]["truck"]["weight"],
      };
    }
  }

  return params;
});

...
</script>
```

Then, add a function to initiate an Amazon Location Service’s client, call the API, and store the results in `route` as GeoJSON.

```vue
<script setup>
...

const calculateRoute = async () => {
  const client = new LocationClient({
    credentials: credentials,
    region: region,
  });

  if (
    requestParams.value.DeparturePosition &&
    requestParams.value.DestinationPosition
  ) {
    const command = new CalculateRouteCommand(requestParams.value);
    const response = await client.send(command);
    const routeFeature = lineString(
      response.Legs.flatMap((leg) => leg.Geometry.LineString),
      response.Summary
    );
    route.features.length = 0;
    route.features.push(routeFeature);
  }
};

...
</script>
```

Afterwards, add three map layers to display positions, their label, and calculated route on the map.

```vue
<script setup>
...

const initializeApp = async () => {
  ...

  map.on("load", () => {
    // Add a layer for rendering departure, waypoint, and destination positions on the map
    map.addLayer({
      id: "positions",
      type: "circle",
      source: { type: "geojson", data: positions },
      paint: {
        "circle-radius": 5,
        "circle-color": "#ffffff",
        "circle-stroke-color": "#00b0ff",
        "circle-stroke-width": 3,
      },
    });

    // Add a layer for rendering position labels on the map
    map.addLayer({
      id: "positions-label",
      type: "symbol",
      source: { type: "geojson", data: positions },
      layout: {
        "text-field": ["get", "title"],
        "text-variable-anchor": ["left"],
        "text-radial-offset": 0.5,
        "text-justify": "auto",
        "text-font": ["Noto Sans Regular"],
      },
    });

    // Add a layer for rendering routes on the map
    map.addLayer(
      {
        id: "route",
        type: "line",
        source: { type: "geojson", data: route },
        layout: {
          "line-join": "round",
          "line-cap": "round",
        },
        paint: {
          "line-color": "#00b0ff",
          "line-width": 5,
          "line-opacity": 0.7,
        },
      },
      "positions"
    );
  });
}

...
</script>
```

Next, add watchers to keep track of state changes, and re-calculate the route and re-render the map layers in reaction to changes.

```vue
<script setup>
...

// Route
watch(route, (value) => {
  map.getSource("route").setData(value);
});

watch(travelModeOptions, async () => {
  await calculateRoute();
});

watch(positions, async (value) => {
  map.getSource("positions").setData(value);
  map.getSource("positions-label").setData(value);
  await calculateRoute();
});

watch(unitOfMeasurementOptions, async () => {
  await calculateRoute();
});

watch(departureTimeOptions, async () => {
  await calculateRoute();
});

watch(timestamp, async () => {
  await calculateRoute();
});

watch(avoidOptions, async () => {
  await calculateRoute();
});

watch(truckHeight, async (value) => {
  if (value === "") {
    truckHeight.value = null;
  }
  await calculateRoute();
});

watch(truckWidth, async (value) => {
  if (value === "") {
    truckWidth.value = null;
  }
  await calculateRoute();
});

watch(truckLength, async (value) => {
  if (value === "") {
    truckLength.value = null;
  }
  await calculateRoute();
});

watch(truckWeight, async (value) => {
  if (value === "") {
    truckWeight.value = null;
  }
  await calculateRoute();
});

...
</script>
```

Finally, add some CSS to style the route calculator.

```vue
<style>
...

.n-card {
  max-width: 400px;
  margin: 10px 0 0 10px;
  box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.08),
    0 3px 6px 0 rgba(0, 0, 0, 0.06), 0 5px 12px 4px rgba(0, 0, 0, 0.04);
}
</style>
```

Now, if you save your project and go to your browser, you will see the final result.

## Wrap up

In this tutorial, you learned how you can build an online route planner that finds optimal routes between an origin, a destination, and up to 23 stops along the way (waypoints) for different modes of transportation, avoidances, and departure times. You can use this project as a starting point for building apps with maps and routing functionality and customize it to fit your purpose. Here are a few things you can do:

* Include a number of different map styles using a layer control — see [this post](https://dev.to/mepa1363/how-to-build-an-interactive-map-using-amazon-location-service-and-maplibre-gl-js-280h) for an example.
* Include a location search widget with geocoding, reverse-geocoding, and autocomplete support to allow users to search for starting point, waypoints, and destination via addresses or geographic coordinates — see [this post](https://dev.to/mepa1363/how-to-add-a-location-search-widget-to-your-web-app-using-amazon-location-service-17jb) for an example.
* Customize the look and feel of the route calculator widget to match your brand and satisfy your use case.

I will end this post with a few notes:

* Routing coverage — Amazon Location Service offers routing services from different data providers. Which data provider should you use for your project? The answer depends on your area of interest. Different data provides may gather and curate their data differently — for example, they may use a combination of authoritative sources, open data, and telemetry data to build their road network and traffic databases. That is why you may notice that a data provider has a better routing coverage or up-to-date traffic information for a region compared to other providers. To choose a data provider, find out what data providers are available in your region, compare their routing coverage in your area of interest, and choose the one that fits your project’s requirements. For example, visit [Esri’s network analysis coverage](https://doc.arcgis.com/en/arcgis-online/reference/network-coverage.htm), [HERE’s car routing coverage](https://developer.here.com/documentation/routing-api/dev_guide/topics/coverage/car-routing.html) and [HERE's truck routing coverage](https://developer.here.com/documentation/routing-api/dev_guide/topics/coverage/truck-routing.html) to get more information about Esri’s and HERE’s coverage in your region.
* Routing with Esri — If Esri is the data provider for your route calculator, the travel distance cannot be greater than 400km. If you specify `Walking` for the travel mode, the departure and destination positions must be within 40km.
* Traffic-aware routing — Amazon Location Service takes traffic into account when calculating a route based on the departure time that you specify. You can specify to depart now, or you can provide a specific time that you want to leave, which will affect the route result by adjusting for traffic at the specified time. If you do not provide any value, it will use the best time of day to travel with the best traffic conditions to calculate the route.
* Map matching — If you specify a position (departure, waypoint, or destination) that is not located on a road, Amazon Location Service will snap it to the nearest road.
* Arrival time — The routing API response includes travel time, reported as `DurationSeconds`. You can use travel time and departure time to calculate arrival time.
* Asset management and tracking use cases — If your application is tracking or routing assets that you use in your business, such as delivery vehicles or employees, you may only use HERE as your data provider. See [section 82 of the AWS service terms](https://aws.amazon.com/service-terms/).

The code for this project is on [GitHub](https://github.com/mepa1363/amazon-location-route-planner/). I’d love to hear your feedback, so please reach out to me if you have any questions or comments.