How to build an interactive map using Amazon Location Service and MapLibre GL JS
Tags: aws, geospatial, webdev, amazonlocationservice
Published: 2022-06-06 21:31:57

Maps are a visual representation of the world around us — they show us where things are and how they overlap and connect. We use interactive maps almost everyday to orient ourselves, discover things nearby, and find a way to get from one place to another. We also use interactive maps to create digital experiences, tell stories, and convey information — from developing a digital twin, to mapping active wildfires, to tracking the spread of infectious diseases, and to analyzing human mobility pattens. 

In this post, I will show you how you can build an interactive map using [Amazon Location Service](https://aws.amazon.com/location/), as the basemap provider, and [MapLibre GL JS](https://maplibre.org/), as the map rendering engine. The map will have navigation controls, a layer control, and a marker with popup information.You can use this project as a starting point for building a web map and customize it to display your stores, real estate properties, or places you have visited on an interactive map.

[Create an AWS account](https://portal.aws.amazon.com/billing/signup), if you don’t have one already, and let’s dive right in.

{% embed https://youtu.be/p1a2AJjdC0w %}

## 1. Create map resources

[Amazon Location Service](https://aws.amazon.com/location/) 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#esri-map-styles) and [HERE map styles] (https://docs.aws.amazon.com/location/latest/developerguide/HERE.html#HERE-map-styles) for more details. We’ll create a few map styles so that we can try them in our app later on. 

To create a map style:

1. Head to the [Amazon Location console] (http://console.aws.amazon.com/location) and select Maps. 
2. On the Maps page, choose **Create map**.
3. Add a **Name** and **Description** (optional) for the new map resource. Use `esri-navigation` for the name of the first map resource. 
4. Choose **Esri Navigation** as the map style.
5. Agree to the [Amazon Location Terms and Conditions] (https://aws.amazon.com/service-terms), then choose **Create map**.

You have now created your first map. Repeat the steps and create four more maps with the following names and map styles:

1. Name: `here-explore-truck`, Map style: **Here Explore Truck**
2. Name: `here-berlin`, Map style: **HERE Berlin**
3. Name: `esri-light-gray-canvas`, Map style: **Esri Light Gray Canvas**
4. Name: `esri-imagery`, Map style: **Esri Imagery**

![The Amazon Location Service Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/90ihedrrotuykra9x1qi.png)
 
## 2. Grant access to map resources

Now, you need to grant access to the map resources that you created in step one 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 fetch map tiles from [Amazon Location Service’s Maps API](https://docs.aws.amazon.com/location/latest/developerguide/map-concepts.html) and render them in the browser.

![The Amazon Cognito Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2u23hzi6aa5ywupxe9cu.png)

To create an Amazon Cognito identity pool:

1. Head to the [Amazon Cognito Console] (http://console.aws.amazon.com/cognito/v2/home) and select **Create new identity pool**. Note that your Amazon Cognito identity pool and Amazon Location Service’s resources must be in the same AWS region, for example `us-west-2`.
2. On the top-right corner, there is a section called **Start from your business case**. From the drop down, choose **Grant access to AWS services**, and choose **Create identity pool**.
3. Enter a **Name** for your identity pool. 
4. From the **Unauthenticated identities** section, choose **Enable access to unauthenticated identities**.
5. Choose **Create pool**.
6. Create IAM roles that you want to use with your identity pool. Expand **View Details** and you’ll see two sections: first section is for creating an IAM role for authenticated identities and the second one is for creating an IAM role for unauthenticated identities.
7. Under the second section, expand the **View Policy Document** section, then choose **Edit** to add your policy.
8. Add the following policy to grant access to your maps resources.
9. Choose **Allow** to create your identity pool. The resulting identity pool follows the syntax `{REGION}:{GUID}`, for example `us-west-2:930a7a7d-bd74-4f36-8575-1db02aa9870d`.

```json
{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Sid":"MapsReadOnly",
         "Effect":"Allow",
         "Action": "geo:GetMap*",
         "Resource": [
            "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/esri-navigation",
            "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/here-explore-truck",
            "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/here-berlin",
            "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/esri-light-gray-canvas",
            "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/esri-imagery"
         ]
      }
   ]
}
```
## 3. Set up project

Create an empty HTML file, `index.html`. Next, add a few dependencies:

* MapLibre GL JS and its associated CSS for rendering the map and map controls.
* AWS SDK for JavaScript for getting credentials from Amazon Cognito.
* AWS Amplify core library for signing HTTP requests using AWS SigV4.

Then, create a new folder, `img`, in your project root directory. Finally, download the images from [this repository] (https://github.com/mepa1363/amazon-location-maplibre-js/tree/main/img) and put them in the `img` folder.

```html
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
    
    <title>Build an interactive map using Amazon Location Service and MapLibre GL JS</title>
    
    <!-- CSS dependencies -->
    <link href='https://unpkg.com/maplibre-gl@2.1.9/dist/maplibre-gl.css' rel='stylesheet' />
    
    <!-- JavaScript dependencies -->
    <script src='https://unpkg.com/maplibre-gl@2.1.9/dist/maplibre-gl.js'></script>
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1145.0.min.js"></script>
    <script src="https://unpkg.com/@aws-amplify/core@4.5.6/dist/aws-amplify-core.min.js"></script>
    
  </head>
  <body>
  
  </body>
</html>
```
## 4. Add auth configurations

To get credentials from Amazon Cognito, first, enter your Cognito identity pool ID and configure your AWS region.

```javascript
<script>
const identityPoolId = "COGNITO_IDENTITY_POOL_ID";

AWS.config.region = identityPoolId.split(":")[0];
</script>
```

Then, instantiate a credential provider using the Cognito identity pool you created earlier.

```javascript
<script>
...

const credentials = new AWS.CognitoIdentityCredentials({
   IdentityPoolId: identityPoolId,
});
</script>
```

Next, add a function to automatically renew credentials before they expire.

```javascript
<script>
...

async function refreshCredentials() {
    await credentials.refreshPromise();
    setTimeout(refreshCredentials, credentials.expireTime - new Date());
}
</script>
```

Finally, add a function to sign the HTTP requests sent to the [Amazon Location Service’s Maps API](https://docs.aws.amazon.com/location/latest/developerguide/map-concepts.html) using AWS SigV4 with the credentials obtained from Amazon Cognito. MapLibre GL JS map instances include a `transformRequest` option, which you can use to intercept and modify requests before they are made.

```javascript
<script>
...

const { Signer } = window.aws_amplify_core;

function transformRequest(url, resourceType) {
    if (resourceType === "Style" && !url.includes("://")) {
        url = `https://maps.geo.${AWS.config.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 };
}
</script>
```
## 5. Add a map

Add a `div` container for the map. MapLibre GL JS will render all map-related HTML elements, such as the map canvas and map controls including navigation controls and attributions, within this container.

```html
<body>
    <div id="map"></div>
</body>
```

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

```css
<style>
    body {
      margin: 0;
      padding: 0;
    }

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

Next, call the `refreshCredentials` function to obtain the credentials when the page first loads and schedule the next credential refresh when they are about to expire. 

```javascript
<script>
...

   async function initializeApp() { 
        await refreshCredentials();
   }

   initializeApp();

</script>
```

Finally, add a new map instance. You can change the values for `center` and `zoom` to adjust your initial map view.

```javascript
<script>
   async function initializeApp() { 
        ...

        const map = new maplibregl.Map({
            container: "map",
            center: [-114.067375, 51.046333],
            zoom: 16,
            style: "esri-navigation",
            hash: true,
            transformRequest,
        });
    }
</script>
```

Now, if you open the HTML file in your browser, you will see an interactive map on the page.

![An interactive map on the web](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jpklf2jykix0ifnbzp0j.png)

## 6. Add navigation controls

Add navigation controls to show zoom buttons and a compass on the map. You have four options for positioning the navigation controls on the map:

* `top-left`
* `top-right`
* `bottom-right`
* `bottom-left`

```javascript
<script> 
    async function initializeApp() {
        ...

        map.addControl(new maplibregl.NavigationControl(), "bottom-right");
    }
</script> 
```

## 7. Add a marker with popup information

Create a new popup and marker instances. Attach the popup to the marker and add them to the map.

```javascript
<script>
    async function initializeApp() {
        ...

        const popup = new maplibregl.Popup({ offset: 35 }).setHTML(
            `<h3>Amazon YYC11</h3>
            <p>225 7 Ave SW, Calgary, AB T2P 2W2</p>
            <img src="img/yyc11.png" width="200" />
            `
        );

        const marker = new maplibregl.Marker()
            .setLngLat([-114.067375, 51.046333])
            .setPopup(popup)
            .addTo(map);
    }
</script>
```

Now, your map should look like the following screenshot.

![An interactive map with navigation controls and a marker with popup information](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yedxdbxgcfmwubqrco13.png)
 
## 8. Add a layer control

The layer control will consist of five cards, each representing a map style, used for switching between different map styles. First, add the following HTML to your code to create the layer control elements.

```html
<body>
  ...

  <div class="layer-control">
    <div class="map-style esri-navigation">
      <img src="img/esri-navigation.png" alt="Esri Navigation" style="width:100%">
      <div class="text">
        Street
      </div>
    </div>
    <div class="map-style here-explore-truck">
      <img src="img/here-explore-truck.png" alt="HERE Explore Truck" style="width:100%">
      <div class="text">
        Truck
      </div>
    </div>
    <div class="map-style here-berlin">
      <img src="img/here-berlin.png" alt="HERE Berlin" style="width:100%">
      <div class="text">
        3D
      </div>
    </div>
    <div class="map-style esri-light-gray-canvas">
      <img src="img/esri-light-gray-canvas.png" alt="Esri Light Gray Canvas" style="width:100%">
      <div class="text">
        Light
      </div>
    </div>
    <div class="map-style esri-imagery">
      <img src="img/esri-imagery.png" alt="Esri Imagery" style="width:100%">
      <div class="text">
        Satellite
      </div>
    </div>
  </div>
  
</body>
```

Then, add the following CSS to style the layer control.

```css
<style>
    ...

    .layer-control {
      position: absolute;
      right: 53px;
      bottom: 30px;
      max-width: 400px;
      padding: 8px;
      border-radius: 5px;
      box-shadow: 0 1px 4px rgb(0 0 0 / 30%);
      background: #fff;
      display: flex;
      justify-content: center;
      z-index: 2;
    }

    .map-style {
      width: 48px;
      height: 60px;
      border-radius: 10px;
      margin: 5px;
    }

    .map-style:hover {
      cursor: pointer;
    }

    .map-style img {
      border-radius: 10px;
    }

    .map-style .text {
      font-size: 12px;
      font-family: Arial, sans-serif;
      color: #333;
      margin: 2px;
      text-align: center;
    }
    
  </style>
```

Finally, add the following logic so that when you click on a map style, the app will change the basemap accordingly.

```javascript
<script>
    async function initializeApp() {     
      ...

      document.querySelector(".map-style.esri-navigation")
        .addEventListener("click", function (event) {
          map.setStyle("esri-navigation")
        });
      document.querySelector(".map-style.here-explore-truck")
        .addEventListener("click", function (event) {
          map.setStyle("here-explore-truck")
        });
      document.querySelector(".map-style.here-berlin")
        .addEventListener("click", function (event) {
          map.setStyle("here-berlin")
        });
      document.querySelector(".map-style.esri-light-gray-canvas")
        .addEventListener("click", function (event) {
          map.setStyle("esri-light-gray-canvas")
        });
      document.querySelector(".map-style.esri-imagery")
        .addEventListener("click", function (event) {
          map.setStyle("esri-imagery")
        });
    }
</script>
```

Now, your app has a layer control that you can use to switch between different map styles.

![An interactive map built with Amazon Location Service and MapLibre GL JS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v5zqo1kjgoejcj5jib39.png)
 
## Wrap up

In this tutorial, we learned how to use Amazon Location Service and MapLibre GL JS to build an interactive map. You can use this project as a starting point for building web maps and customize it to fit your purpose. You can also add more things to it, for example, you can add more markers to highlight your favourite hiking trails, advertise your rental properties, or display your stores on a map.
You can check out this [GitHub repository] (https://github.com/mepa1363/amazon-location-maplibre-js), if you want to dive into the code.  I’d love to hear your feedback, so please reach out to me if you have any questions or comments.

I will end this post with a few tips:

* Map styles — Amazon Location Service offers a set of different map styles; how do you choose a map style for your project? The answer depends on your use case:
  * If your application uses map as a main source of information like a rental property app, ride sharing app, or a routing and navigation app then use a map style with detailed information about roads, landmarks, and buildings. Use **Esri Navigation**, **Esri Imagery**, or **HERE Berlin** for such use cases. 
  * If your application is focused on transportation by trucks, then use a map style that highlights truck-related attributes and restrictions such as width and height. Use **HERE Explore Truck** for this use case.
  * If your application uses map as a background for data visualization then use a map style with neutral colors and minimal features to draw attention to your thematic content. Use **Esri Light Gray Canvas** or **Esri Dark Gray Canvas** for such use cases.
* Data quality and map coverage — Amazon Location Service offers map styles from different data providers. Which data provider should you use for your project? The answer depends on your area of interest. Each data provider gathers and curates their data using different techniques — for example they may use a combination of authoritative sources, open data, and AI-assisted mapping to compile their databases and basemaps. So the quality of data sources and the accuracy of their methods have a direct effect on the quality of their basemaps. That is why you may notice that a data provider has a better map coverage or up-to-date, higher resolution imagery for a region compared to other providers. To choose a data provider, pick a few map styles from different providers and compare their map coverage in your area of interest and choose the one that fits your project’s requirements.
* 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/).
* Service quotas — There are limits on the number of Amazon Location Service’s resources you can have per AWS account and AWS region. There are also limits on the number of requests per second you can send to an Amazon Location Service’s APIs — see [Amazon Location Service quotas] (https://docs.aws.amazon.com/location/latest/developerguide/location-quotas.html) for more information. However, the quotas for all the APIs offered by Amazon Location Service are adjustable. This means that you can use the [Service Quotas console] (https://console.aws.amazon.com/servicequotas/home?region=us-east-1#!/services/geo/quotas) to request for an increase, if you require a higher quota.
* Deployment — When you deploy your website you may want to limit browser access to your Amazon Location Service’s resources to certain URLs. To do so, add the following condition to the IAM policy you created in step 2 to deny access to your map resources from all referring websites except `example.com`.

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "MapsReadOnly",
            "Effect": "Allow",
            "Action": "geo:GetMap*",
            "Resource": [
                "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/esri-navigation",
                "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/here-explore-truck",
                "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/here-berlin",
                "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/esri-light-gray-canvas",
                "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/esri-imagery"
            ],
            "Condition": {
                "StringLike": {
                    "aws:referer": [
                        "https://example.com/*",
                        "https://www.example.com/*"
                    ]
                }
            }
        }
    ]
}
```