DEV Community

Cover image for How to build an online route planner with Amazon Location Service
abraham poorazizi
abraham poorazizi

Posted on • Updated on

How to build an online route planner with Amazon Location Service

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 app using Amazon Location Service, as the basemap provider and routing API, MapLibre GL JS, as the map rendering library, and Naive UI, 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

Prerequisites

We will create the AWS resources for this project using AWS CLI. Before we start

1. Create a map resource

Amazon Location Service’s Maps API offers a set of map styles, professionally designed to support different applications and use cases — see Esri map styles and HERE map styles for more details. Now, create a map resource using the map style Esri Navigation.

aws location create-map \
--map-name=esri-map-navigation \
--configuration Style=VectorEsriNavigation \
--profile={YOUR_AWS_PROFILE}
Enter fullscreen mode Exit fullscreen mode

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

{
    "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"
}
Enter fullscreen mode Exit fullscreen mode

2. Create a route calculator resource

Amazon Location Service’s Routes API 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.

aws location create-route-calculator \
--calculator-name "esri-route-calculator" \
--data-source "Esri" \
--profile={YOUR_AWS_PROFILE}
Enter fullscreen mode Exit fullscreen mode

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.

{
    "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"
}
Enter fullscreen mode Exit fullscreen mode

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 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 to explore more options. First, create an Amazon Cognito identity pool.

aws cognito-identity create-identity-pool \
--identity-pool-name routing-app \
--allow-unauthenticated-identities \
--profile={YOUR_AWS_PROFILE}
Enter fullscreen mode Exit fullscreen mode

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.

{
    "IdentityPoolId": "{IDENTITY_POOL_ID}",
    "IdentityPoolName": "routing-app",
    "AllowUnauthenticatedIdentities": true,
    "IdentityPoolTags": {}
}
Enter fullscreen mode Exit fullscreen mode

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). 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.

{
   "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"
            }
         }
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s create a new IAM role.

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}
Enter fullscreen mode Exit fullscreen mode

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.

{
    "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"
                        }
                    }
                }
            ]
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

{
   "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"
         ]
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode

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

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}
Enter fullscreen mode Exit fullscreen mode

Finally, add the IAM role to your identity pool.

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}
Enter fullscreen mode Exit fullscreen mode

4. Set up a new project

Create a new Vue project using Vite.

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

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

✔ Select a variant: › vue
✔ Project name: amazon-location-route-planner
✔ Select a framework: › vue
Enter fullscreen mode Exit fullscreen mode

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

cd amazon-location-route-planner
npm install
Enter fullscreen mode Exit fullscreen mode

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

<template>
</template>

<script setup>
</script>

<style>
</style>
Enter fullscreen mode Exit fullscreen mode

Finally, add the following configuration to vite.config.

export default defineConfig({
  ...

  define: {
    global: {}
  }
});
Enter fullscreen mode Exit fullscreen mode

5. Add auth configurations

First, install a few dependencies.

npm install @aws-amplify/core @aws-sdk/client-cognito-identity @aws-sdk/credential-provider-cognito-identity
Enter fullscreen mode Exit fullscreen mode

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

const identityPoolId = "{IDENTITY_POOL_ID}";

export { identityPoolId };
Enter fullscreen mode Exit fullscreen mode

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
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 };
Enter fullscreen mode Exit fullscreen mode

6. Add a map

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

npm install maplibre-gl
Enter fullscreen mode Exit fullscreen mode

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

...

const mapName = "esri-map-navigation";

export { identityPoolId, mapName };
Enter fullscreen mode Exit fullscreen mode

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

<template>
   <div id="map"></div>
</template>
Enter fullscreen mode Exit fullscreen mode

Afterwards, add a new map instance with navigation controls.

<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>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

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 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 to see the API response elements.

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

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

npm install -D naive-ui vfonts @vicons/carbon
Enter fullscreen mode Exit fullscreen mode

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

...

const routeCalculatorName = "esri-route-calculator";

export { identityPoolId, mapName, routeCalculatorName };
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

Next, import the required configurations, functions, and components into App.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>
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

Finally, add some CSS to style the route calculator.

<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>
Enter fullscreen mode Exit fullscreen mode

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 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 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, HERE’s car routing coverage and HERE's truck routing coverage 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.

The code for this project is on GitHub. I’d love to hear your feedback, so please reach out to me if you have any questions or comments.

Top comments (2)

Collapse
 
hhkaos profile image
Raul Jimenez Ortega

Great article @mepa1363 !

I'm so new to the AWS environment that I didn't know about the AWS CLI 😅. When I followed the Quick start with Amazon Location Service I spent more time than I would have liked using the UIs. Just for that alone, the article was worth reading! ❤️.

Collapse
 
mepa1363 profile image
abraham poorazizi

Thanks Raul. Glad to hear it. I'm in the same boat; I'd like to do everything in my terminal and never open the console 😁