Scroll Top

Level Up Your Web App: Building a Geo-tastic Map with Angular, Material UI, and AWS

Feature-Image

Greetings, fellow developers! In this journey, we’ll dive into the Angular universe, weaving together the threads of Material UI, AWS Cognito, Amplify.js, and AWS Map and Place Index to create a captivating geolocation web application. Join us as we embark on a quest filled with maps and markers, suitable for both seasoned professionals and budding beginners.

Let’s get started!

1. Setting the Stage: Angular Essentials

In this section, we will dive into the foundational aspects of Angular 16, set up our project, and install essential dependencies. Let us begin by laying the groundwork for our geolocation web application.

Overview of Angular: Angular is a comprehensive framework for building client-side web applications. It provides developers with a structured approach to application development, using TypeScript to enable robust and scalable code.

Setting Up Our Angular Project: To create a new Angular project, we will use the Angular CLI (Command Line Interface), which streamlines project creation and management. Open your terminal and run the following command:

ng new geolocation-app

This command generates a new Angular project named `geolocation-app` in the current directory.

cd geolocation-app

Next, we will install Angular Material, a UI component library that integrates seamlessly with Angular.

ng add @angular/material

Follow the prompts to select a custom theme and configure global typography styles.

Once this is complete, you will have the base Angular + Material UI setup ready to begin development.

We will also need libraries for map rendering and authentication with AWS.

For maps and markers, we will use MapLibre GL.
For interacting with AWS APIs, we will use AWS Amplify JS.

The code provided is not limited to AWS Amplify JS. A separate Credentials Provider file has been included, which can be used even if Amplify is not used for authentication in your existing project.

All you need are the user’s authentication credentials. You can modify the provided Credentials Provider to use any authentication credentials that suit your requirements.

We will need to install the following libraries using npm:

npm i aws-amplify  @aws-amplify/geo  maplibre-gl  maplibre-gl-js-amplify
npm i @types/mapbox__mapbox-gl-draw --save-dev

For the project file structure, please refer to the provided GitHub repository.

2. Diagrams:

2.1. Architecture Diagram:

Demo App Architecture Diagram
Demo App Architecture Diagram

 

2.2. Flow Diagram

Demo App Flow Diagram
Demo App Flow Diagram

3. AWS Setup:

The setup requires the following AWS services and configurations:

  • Cognito User Pool
  • Cognito Identity Pool
  • Permissions to create and update roles and access policies
  • AWS Location Service (Map and Place Index)

You can easily find up-to-date guides online to set these up.

The process has become quite straightforward, as shown in the screenshot below:

3.1. Cognito User Pool and App Client:

AWS — User Pool — Default
AWS — User Pool — Default

Set up a new App Client if it has not already been created.

AWS — User Pool — App Client
AWS — User Pool — App Client

For better security, it is recommended to use `ALLOW_USER_SRP_AUTH`.

Please consider this option if you are developing a new application and if it supports this authentication method for your use case. 

3.2. Cognito Identity Pool:

AWS — Identity Pool — Step 1
AWS — Identity Pool — Step 1

 

AWS — Identity Pool — Step 2
AWS — Identity Pool — Step 2

 

AWS — Identity Pool — Step 3
AWS — Identity Pool — Step 3

In this step, you will need to select the User Pool and App Client that were created in section 3.1: Cognito User Pool and App Client.

AWS — Identity Pool — Step 4
AWS — Identity Pool — Step 4

3.3. Map (Location Service):

Since the update on 2 Apr 2025 (linked here),AWS no longer requires the creation of a Map resource. However, to help users understand the original functionality, this option is still available as a legacy feature.

Below are the details for creating a legacy Map resource.

API keys can also be configured for accessing Map/Location Service resources.

AWS — Map (Location Service)
AWS — Map (Location Service)

When selecting a map provider, AWS offers three primary options:

Each provider has its own advantages, disadvantages, coverage areas, and overall map look and feel. The geographical coverage also varies significantly between providers.

This AWS document  on map data providers offers detailed insights into how each option looks and the regions they cover. ESRI appears to be a widely used provider. Open Data uses open-source datasets, whereas the other two options do not.

Additionally, there is another provider called Grab Maps, which offers map data specifically for Southeast Asia. For this reason, it is only available for resources created in the ap-southeast-1 region.

As a result, it is not suitable for applications that need to load maps for other geographical regions.

Below is a quick summary comparing Google Maps, AWS Location Service, and Azure Maps.

Location/Map Service — GCP vs AWS vs Azure — Comparision Summary
Location/Map Service — GCP vs AWS vs Azure — Comparision Summary

There are many factors to consider when selecting a service—not just which is the best or easiest to use.

In this blog, AWS has been chosen due to its seamless integration with the AWS ecosystem, which aligns with the environment used during development.

3.4. Map — Place Index (Location Service):

AWS — Place Index (Location Service)
AWS — Place Index (Location Service)

3.5. AWS IAM Policy for Map Read-Only Access for Users

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "MapReadOnlyAccess",
            "Effect": "Allow",
            "Action": [
                "geo:GetMapGlyphs",
                "geo:GetMapSprites",
                "geo:GetMapStyleDescriptor",
                "geo:GetMapTile"
            ],
            "Resource": "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:map/{MAP_NAME}",
            "Condition": {
                "ForAnyValue:StringLike": {
                    "aws:Referer": "http://localhost:4200/*"
                }
            }
        },
        {
            "Sid": "PlaceIndexReadOnlyAccess",
            "Effect": "Allow",
            "Action": [
                "geo:SearchPlaceIndexForText",
                "geo:SearchPlaceIndexForSuggestions",
                "geo:SearchPlaceIndexForPosition",
                "geo:GetPlace"
            ],
            "Resource": "arn:aws:geo:{AWS_REGION}:{AWS_ACCOUNT_ID}:place-index/{PLACE_INDEX_NAME}",
            "Condition": {
                "ForAnyValue:StringLike": {
                    "aws:Referer": "http://localhost:4200/*"
                }
            }
        }
    ]
}

This is an AWS IAM policy that provides limited read-only access to Amazon Location Service for an application running at: http://localhost:4200.

[Example] IAM Policy
[Example] IAM Policy

3.6. Applying IAM Policy to AWS Cognito Identity Pool

[Example] AWS Cognito Identity Pool User Access
[Example] AWS Cognito Identity Pool User Access
[Example] AWS Cognito Identity Pool — Role — New Attached Policy
[Example] AWS Cognito Identity Pool — Role — New Attached Policy

3.7. [ALTERNATE AUTHORIZATION PATH] API Keys for Accessing Location Service Resources

It is also possible to access Location Service resources using API keys.

If you prefer not to manage access through policies and user identities, this can be a simpler alternative.

AWS Location Service → API Keys
AWS Location Service → API Keys

 

You can create API keys with specific permissions for maps, place indexes, and routes, and use those keys in your application to grant controlled access to these resources.

AWS Location Service → Create API Key
AWS Location Service → Create API Key

As shown above, you can configure your API Key.

The official AWS blog on implementing API keys for accessing Location Service resources can be found here: (https://aws.amazon.com/blogs/mobile/build-a-geospatial-application-with-amazon-location-service-api-keys).

If this is something you want to implement, please follow the linked AWS guide on how to use Location Service API Keys in your application.

For the remaining markers and other configurations, you can follow the details described below.

4. Angular Project:

I will focus on the map integration files, presenting their code along with detailed explanations.

For other required files, please refer to the GitHub repository mentioned below.

4.01. Environment File Setup

src/environments/environment.ts

import { MAP_STYLES } from "maplibre-gl-js-amplify/lib/esm/constants";
import { Environment } from "./types";

// Define environment configuration
const _environment: Environment = {
    environmentFile: 'environment.ts',
    production: false,
    aws: {
        region: 'REGION GOES HERE',
        amplifyConfig: {
            Auth: {
                Cognito: {
                    userPoolId: 'USER_POOL_ID GOES HERE',
                    userPoolClientId: 'CLIENT ID GOES HERE',
                    identityPoolId: 'IDENTITY_POOL_ID GOES HERE',
                    signUpVerificationMethod: 'code',
                    loginWith: { email: true, username: true },
                    allowGuestAccess: true,
                }
            }
        },
        signInOptions: {
            authFlowType: 'USER_PASSWORD_AUTH',
        },
        mapResource: {
            mapName: 'YOUR MAP RESOURCE NAME GOES HERE',
            mapStyle: MAP_STYLES.ESRI_NAVIGATION, // YOUR MAP STYLE GOES HERE
            placeIndexName: 'YOUR PLACE INDEX NAME GOES HERE'
        },
    },
};
// Export the environment configuration
export const environment = Object.freeze(_environment);

 

Please note:

In the above configuration file, I have specified the following line: loginWith: { email: true, username: true }

In this configuration, `username: true` should only be enabled if it has been allowed in Step 1 of your Cognito configuration (3.1 Cognito User Pool and App Client) and you plan to use a username for authentication.

Otherwise, we can omit this option.

4.02. [Optional Bonus] AWS Credentials Provider

src/app/utils/AwsSdkCredentialsProvider.ts

We do not currently use this file in the application; however, we include it for reference, especially for those interested in building a credentials provider using an existing Access Key ID, Secret Access Key, and Session Token.

These elements form the core of an authenticated user’s access and identity. This provider can be used to supply authentication credentials for AWS Maps API calls. To use it, you simply need to populate the credentials object with the Access Key ID, Secret Access Key, and Session Token.

This provider can also be extended to support guest (unauthenticated) access through an AWS Cognito Identity Pool. 

import { CredentialsAndIdentityId, CredentialsAndIdentityIdProvider } from '@aws-amplify/core';

export class AwsSdkCredentialsProvider implements CredentialsAndIdentityIdProvider {
    constructor(private cognitoService: CognitoService) {}
    async getCredentialsAndIdentityId(): Promise {
        return new Promise((resolve, reject) => {
   const userCredentials = ;
            if (userCredentials) {
                resolve({
                    credentials: {
                        accessKeyId: userCredentials.accessKeyId,
                        secretAccessKey: userCredentials.secretAccessKey,
                        sessionToken: userCredentials.sessionToken,
                    },
                });
            } else {
                reject('Credentials missing!');
            }
        });
    }
    // Implement this to clear any cached credentials and identityId.
    clearCredentialsAndIdentityId(): void {
        // Method to clear cached credentials and identityId, if needed.
    }
}

This TypeScript code represents the AwsSdkCredentialsProvider class, which implements the CredentialsAndIdentityIdProvider interface from @aws-amplify/core. Here’s a breakdown of the key components:

  • constructor: Initializes the AwsSdkCredentialsProvider with a CognitoService instance injected as a dependency.
  • getCredentialsAndIdentityId: Asynchronously retrieves credentials and the identity ID from the Cognito service’s authentication session. If successful, it resolves with the credentials and identity ID; otherwise, it rejects with an error.
  • clearCredentialsAndIdentityId: A placeholder method to clear any cached credentials and identity ID, if needed.

This class is responsible for providing AWS SDK-compatible credentials and an identity ID for authenticated users, facilitating seamless integration with AWS services within the application.

4.03. Cognito Service

src/app/services/cognito.service.ts

import { Injectable } from '@angular/core';
import { Amplify } from 'aws-amplify';
import { AuthSession } from '@aws-amplify/core/dist/esm/singleton/Auth/types';
import { signIn, signUp, signOut, confirmSignUp, getCurrentUser, AuthUser, ConfirmSignUpOutput, SignUpOutput, resendSignUpCode, ResendSignUpCodeOutput, SignInOutput, fetchAuthSession } from 'aws-amplify/auth';
import { environment } from 'src/environments/environment';

// Interface for user data
export interface IUser {
    email: string;
    password?: string;
    showPassword?: boolean;
    code?: string;
    name?: string;
}

// Local storage key for user logged-in status
export const ls_key_is_user_logged_in = 'IS_USER_LOGGED_IN';

@Injectable({
    providedIn: 'root',
})
export class CognitoService {
    constructor() {
        // Configure Amplify with environment settings
        Amplify.configure(environment.aws.amplifyConfig);
    }
    // Sign up user
    public signUp(user: IUser): Promise {
        return signUp({
            username: user.email,
            password: user.password || '',
        });
    }
    // Confirm sign up with verification code
    public confirmSignUp(user: IUser): Promise {
        return confirmSignUp({ username: user.email, confirmationCode: user.code || '' });
    }
    // Resend sign up verification code
    public resendSignUpCode(user: IUser): Promise {
        return resendSignUpCode({ username: user.email });
    }
    // Sign in user
    public signIn(user: IUser): Promise {
        const signInRequest = signIn({
            username: user.email,
            password: user.password,
            options: { authFlowType: 'USER_PASSWORD_AUTH' }
        });
        signInRequest.then((res) => {
            if (res.isSignedIn) {
                localStorage.setItem(ls_key_is_user_logged_in, 'true');
            } else {
                localStorage.removeItem(ls_key_is_user_logged_in);
            }
        }).catch((err) => {
            console.log('CognitoService.signIn(): err', err);
            localStorage.removeItem(ls_key_is_user_logged_in);
        });
        return signInRequest;
    }
    // Sign out user
    public signOut(): Promise {
        return signOut().then(() => {
            localStorage.removeItem(ls_key_is_user_logged_in);
        });
    }
    // Get current authenticated user
    public getUser(): Promise {
        const user = getCurrentUser();
        user.then((user) => {
            if (user) {
                localStorage.setItem(ls_key_is_user_logged_in, 'true');
            } else {
                localStorage.removeItem(ls_key_is_user_logged_in);
            }
        }).catch((err) => {
            console.log('CognitoService.getUser(): err', err);
            localStorage.removeItem(ls_key_is_user_logged_in);
        });
        return user;
    }
    // Get authentication session
    public getAuthSession(): Promise {
        const session = fetchAuthSession();
        session.then(() => {
            localStorage.setItem(ls_key_is_user_logged_in, 'true');
        }).catch((err) => {
            console.log('CognitoService.getUserSession(): err', err);
            localStorage.removeItem(ls_key_is_user_logged_in);
        });
        return session;
    }
    // Check if user is authenticated
    public isAuthenticated(): Promise {
        return this.getUser()
            .then((user) => {
                if (user) {
                    return true;
                } else {
                    return false;
                }
            }).catch(() => {
                return false;
            });
    }
    // Check if user is logged in (based on local storage)
    isUserLoggedIn(): boolean {
        return localStorage.getItem(ls_key_is_user_logged_in) === 'true';
    }
}

This TypeScript code represents the CognitoService class in your Angular application and is responsible for handling authentication-related operations using AWS Cognito. Here’s a breakdown of its key functionalities:

  • signUp: Registers a new user with AWS Cognito.
  • confirmSignUp: Verifies the confirmation code sent during the sign-up process.
  • resendSignUpCode: Resends the sign-up verification code to the user’s email.
  • signIn: Authenticates a user using their email and password.
  • signOut: Signs out the currently authenticated user.
  • getUser: Retrieves the current authenticated user.
  • getAuthSession: Retrieves the current authentication session.
  • isAuthenticated: Checks whether a user is currently authenticated.
  • isUserLoggedIn: Checks whether a user is logged in based on a local storage flag.

Note:
Here, I have used `localStorage` to store temporary credentials in the application. For a better approach, you can use global state management in Angular.

For managing global state in Angular, one solution is RxAngular Global State:
https://www.rx-angular.io/docs/state/recipes/use-rxstate-as-global-state

This provides support for handling events throughout the entire Angular application, allowing you to maintain a global state across the application or within specific sections, depending on your needs.

For the authentication section, a recommended solution is the Amplify Auth Event Hub:https://docs.amplify.aws/angular/build-a-backend/auth/auth-events/
Using this provides a better way to handle different events that occur during authentication.

4.04. Map Component — HTML

src/app/components/map/map.component.html

<!-- MAP COMPONENT -->
<h1>MAP COMPONENT</h1>
<div class="map-wrapper">
    <!-- Div element to contain the map -->
    <div #map class="my-map"></div>
</div>
<div class="actions">
    <!-- Button to handle adding predefined locations -->
    <button (click)="handleAddPredefinedLocationClick()" mat-stroked-button>
        Add Predefined Location
    </button>
    <!-- Button to handle removing all map markers -->
    <button (click)="removeAllMapMarkers()" mat-stroked-button>
        Remove all Markers
    </button>
</div>

4.05. Map Component — Logic (Typescript)

src/app/components/map/map.component.ts

import { AfterViewInit, Component, ElementRef, EventEmitter, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Amplify, ResourcesConfig } from '@aws-amplify/core';
import { Geo } from '@aws-amplify/geo';
import { SearchByTextOptions, Place } from '@aws-amplify/geo/dist/esm/types/Geo';
import { LegacyConfig } from '@aws-amplify/core/internals/utils';
import { FitBoundsOptions, LngLatBounds, Map, MapOptions, Marker, NavigationControl } from 'maplibre-gl';
import { createAmplifyGeocoder, createMap, drawPoints } from 'maplibre-gl-js-amplify';
import { NamedLocation } from 'maplibre-gl-js-amplify/lib/esm/types';
import { DrawPointsOutput } from 'maplibre-gl-js-amplify/lib/esm/drawPoints';
import { MAP_STYLES } from 'maplibre-gl-js-amplify/lib/esm/constants';
import { environment } from 'src/environments/environment';
import { MapMarkerCustom1Component } from './markers/map-marker-custom-1/map-marker-custom-1.component';
import { MapMarkerCustom2Component } from './markers/map-marker-custom-2/map-marker-custom-2.component';
import { MapMarkerCustom3Component } from './markers/map-marker-custom-3/map-marker-custom-3.component';
import { MapMarkerAmplifyMapLibreGLComponent } from './markers/map-marker-amplify-maplibre-gl/map-marker-amplify-maplibre-gl.component';
import { CUSTOM_MAP_MARKER, MAP_MARKER_TYPE, MapConfig } from './types';

/**
 * Custom map marker classes defined in their respective components
 */
export const CUSTOM_MAP_MARKER_CLASSES = Object.freeze({
 [CUSTOM_MAP_MARKER.CUSTOM_HTML_1]: 'map-marker-custom-1',
 [CUSTOM_MAP_MARKER.CUSTOM_HTML_2]: 'map-marker-custom-2',
 [CUSTOM_MAP_MARKER.CUSTOM_HTML_3]: 'map-marker-custom-3',
 [CUSTOM_MAP_MARKER.MAPLIBRE_GL_AMPLIFY]: 'map-marker-maplibre-gl',
});
const AWS_LOCATION_SERVICE_ADDRESS_SEARCH_OPTIONS: SearchByTextOptions = {
 providerName: 'AmazonLocationService',
 countries: ['USA'],
 language: 'EN',
};
const MAP_CONFIG: MapConfig = {
 markerColor: '#E74B3C',
 mapLibreAmplifyMarkersSourceName: 'Map_Libre_Amplify_Markers',
 markerType: MAP_MARKER_TYPE.CUSTOM_HTML,
 customMarkerVariant: CUSTOM_MAP_MARKER.CUSTOM_HTML_1,
 getMapBoundingOptions: (
  _markerType = MAP_CONFIG.markerType,
  _customMarkerVariant = MAP_CONFIG.customMarkerVariant
 ) => {
  const options: FitBoundsOptions = {
   maxZoom: 14,
   padding: {
    bottom: 50,
    top: 50,
    left: 50,
    right: 70,
   },
   duration: 1500,
  };
  return options;
 },
};

@Component({
 selector: 'app-map',
 templateUrl: './map.component.html',
 styleUrls: ['./map.component.scss'],
})
export class MapComponent implements OnInit, AfterViewInit, OnChanges {
 @ViewChild('map') private mapContainer!: ElementRef;
 inputAddresses: string[];
 inputAddressesExists = false;
 map: Map;
 mapStyle: string;
 mapInitialState: Partial = {
  container: undefined,
  center: {
   lng: -73.6546634,
   lat: 45.4859129,
  },
  zoom: 14,
  interactive: true,
 };
 addOnClickFixedLocations: {
  currentIndex: number
  locations: NamedLocation[]
 } = {
   currentIndex: 0,
   locations: [
    {
     title: 'Location 1',
     address: 'Location 1, Location 1, Location 1, Location 1, Location 1, Location 1, US',
     coordinates: [-74.020253, 45.779222],
    },
    {
     title: 'Location 2',
     address: 'Location 2, Location 2, Location 2, Location 2, Location 2, Location 2, US',
     coordinates: [-69.656862, 47.702024],
    },
    {
     title: 'Location 3',
     address: 'Location 3, Location 3, Location 3, Location 3, Location 3, Location 3, US',
     coordinates: [-61.9720506, 46.452625],
    },
   ]
  };
 mapMarkers: Marker[] = [];
 namedLocations: NamedLocation[] = [];
 mapDrawPoints: DrawPointsOutput;
 mapBounds = new LngLatBounds();
 locationSearches: { [x: string]: Promise<Place[]> | Place | undefined } = {};
 drawPointInitEventEmitter: EventEmitter;
 bufferDrawPoints: NamedLocation[] = [];
 constructor(
  private route: ActivatedRoute,
  private readonly viewContainerRef: ViewContainerRef
 ) {
  this.mapStyle = `https://maps.geo.${environment.aws.region}.amazonaws.com/maps/v0/maps/${environment.aws.mapResource.mapName}/style-descriptor`;
 }

 ngOnInit(): void {
  this.route.data.forEach((data) => {
   if (data['inputAddresses']) {
    this.inputAddresses = data['inputAddresses'];
    this.inputAddressesExists = true;
   }
  })
  this.configureAmplify();
 }

 ngAfterViewInit(): void {
  this.initMap();
 }

 /**
  * Track changes
  * @param changes
  */
 ngOnChanges(changes: SimpleChanges): void {
  /**
   * Handle input locations changes
   */
  if (this.inputAddresses?.length) {
   this.inputAddresses.forEach((address, i) => {
    if (address) {
     if (this.locationSearches[address]) {
      /**
       * Location already exist in previously searched locations,
       *  and it should already be present in the Map as a Marker/DrawPoint,
       *  so there is no need to do anything.
       */
     } else {
      this.locationSearches[address] = this.searchByAddress(address);
      (this.locationSearches[address] as Promise<Place[]>)
       .then(res => {
        if (res?.length) {
         const firstSearchResult = res[0];
         this.locationSearches[address] = firstSearchResult;
         const firstNamedLocation = placeToNamedLocation(firstSearchResult, address, address);
         this.addLocationMarkerInMap(firstNamedLocation, i + 1);
        } else {
         console.warn('Location not found:', address);
        }
       })
       .catch(error => {
        console.error(error);
        this.locationSearches[address] = undefined;
       });
     }
    } else {
     console.warn('Location address not searchable:', address);
    }
   });
  }

  /**
   * Turn Place result to NamedLocation for showing it in map
   * @param place
   * @param titleOverride
   * @param addressOverride
   * @returns
   */
  function placeToNamedLocation(place: Place, titleOverride?: string, addressOverride?: string) {
   return {
    title: titleOverride,
    coordinates: place.geometry?.point,
    address: addressOverride,
   } as NamedLocation;
  }
 }

 /**
  * Amplify configuration with credentials and Location Service config
  */
 configureAmplify(): void {
  console.info('env:', environment);
  Amplify.configure(
   {
    ...Amplify.getConfig(),
    Geo: {
     LocationService: {
      maps: {
       items: {
        [environment.aws.mapResource.mapName]: {
         // REQUIRED - Amazon Location Service Map resource name
         style: MAP_STYLES.ESRI_NAVIGATION, // REQUIRED - String representing the style of map resource
         // Other Styles: https://docs.aws.amazon.com/location/latest/APIReference/API_MapConfiguration.html
        },
       },
       default: environment.aws.mapResource.mapName, // REQUIRED - Amazon Location Service Map resource name to set as default
      },
      searchIndices: {
       items: [environment.aws.mapResource.placeIndexName], // REQUIRED - Amazon Location Service Place Index name
       default: environment.aws.mapResource.placeIndexName, // REQUIRED - Amazon Location Service Place Index name to set as default
      },
      // NOTE: THERE IS A BUG IN AWS LIBRARY SO NEED TO ADD THE SAME `searchIndices` as `search_indices`
      search_indices: {
       items: [environment.aws.mapResource.placeIndexName], // REQUIRED - Amazon Location Service Place Index name
       default: environment.aws.mapResource.placeIndexName, // REQUIRED - Amazon Location Service Place Index name to set as default
      },
      region: environment.aws.region, // REQUIRED - Amazon Location Service Region
     },
    },
   } as ResourcesConfig | LegacyConfig,
   // For authentication using Custom Credentials Provider
   // Can also be used for unauthenticated guest sessions
   // {
   //  Auth: { credentialsProvider: new AwsSdkCredentialsProvider() },
   // }
  );
 }

 /**
  * Initiate Map
  */
 async initMap(): Promise {
  try {
   this.map = await createMap({
    container: this.mapContainer.nativeElement,
    style: this.mapStyle,
    center: this.mapInitialState.center,
    zoom: this.mapInitialState.zoom,
    interactive: this.mapInitialState.interactive,
    attributionControl: false,
    // transformRequest: await getMapRequestTransformerForAuth(),
   });
   this.map.on('load', (event) => {
    console.log('MapLoadEvent:', event);
    if (this.inputAddressesExists) {
     this.ngOnChanges({});
    }
   });
   this.addMapControls();
   this.onClickAddMarker();
   if (MAP_CONFIG.markerType === MAP_MARKER_TYPE.MAPLIBRE_GL_AMPLIFY_DEFAULT) {
    this.initDrawPoints();
   }
  } catch (error) {
   console.error(error);
  }

  // Not needed as of now because we are using AWS global credential config through Amplify for authentication
  /**
   * A higher-order function for signing request URL with AWS signer
   * @returns a function that accepts base URL of 'amazonaws.com' and returns a pre-signed URL
   */
  // async function getMapRequestTransformerForAuth(): Promise {
  //  const credentials = (await new AwsSdkCredentialsProvider().getCredentialsAndIdentityId()).credentials;
  //  return (url: string) => {
  //   // Only sign aws URLs
  //   if (url.includes('amazonaws.com')) {
  //    return {
  //     url: presignUrl(
  //      { url: new URL(url) },
  //      {
  //       credentials: {
  //        accessKeyId: credentials.accessKeyId,
  //        secretAccessKey: credentials.secretAccessKey,
  //        sessionToken: credentials.sessionToken,
  //       },
  //       signingRegion: this.env.AWS_REGION,
  //       signingService: 'geo',
  //      }
  //     ).toString(),
  //    };
  //   }
  //  };
  // }
 }

 /**
  * Add map controls in map
  */
 addMapControls(): void {
  this.map.addControl(
   new NavigationControl({ showCompass: true, showZoom: true, visualizePitch: true }),
   'top-right'
  );
  // Add search controls
  this.map.addControl(createAmplifyGeocoder());
 }
 /**
  * Initialize Draw Points in the map for AmpLibre Amplify Markers
  */
 initDrawPoints(): void {
  this.map.on('load', () => {
   this.mapDrawPoints = drawPoints(MAP_CONFIG.mapLibreAmplifyMarkersSourceName, this.namedLocations, this.map, {
    showCluster: false,
    // clusterOptions: { showCount: true, smThreshold: 1, mdThreshold: 3, lgThreshold: 5 },
    unclusteredOptions: { showMarkerPopup: true, defaultColor: MAP_CONFIG.markerColor },
    autoFit: true,
   });
   this.drawPointInitEventEmitter?.emit(true);
  });
 }

 /**
  * Add Map marker on when clicking on map
  */
 onClickAddMarker(): void {
  this.map.on('click', e => {
   console.log('Map Click Event:', e);
   this.addLocationMarkerInMap({ coordinates: [e.lngLat.lng, e.lngLat.lat] });
  });
 }

 /**
  * Add Map marker on button click
  */
 handleAddPredefinedLocationClick(): void {
  if (this.addOnClickFixedLocations.currentIndex < this.addOnClickFixedLocations.locations.length) {
   const nextLocation = this.addOnClickFixedLocations.locations[this.addOnClickFixedLocations.currentIndex];
   this.addLocationMarkerInMap(nextLocation, this.addOnClickFixedLocations.currentIndex + 1, CUSTOM_MAP_MARKER.MAPLIBRE_GL_AMPLIFY);
   this.addOnClickFixedLocations.currentIndex++;
  } else {
   this.addOnClickFixedLocations.currentIndex = 0;
   this.removeAllMapMarkers();
  }
 }

 /**
  * Search place(s) on AWS Location Service using Map Index
  * @param addressStr Address to search for
  * @returns Promise of places found with given address string
  */
 searchByAddress(addressStr: string): Promise<Place[]> | undefined {
  if (addressStr) {
   return Geo.searchByText(addressStr, AWS_LOCATION_SERVICE_ADDRESS_SEARCH_OPTIONS);
  }
  return undefined;
 }

 /**
  * Add location marker in the map
  * @param namedLocation Named Location to add in the Map
  * @param number Number to show on marker, if any
  * @param customMarkerType Marker Type if you want to specify a different custom marker
  */
 addLocationMarkerInMap(namedLocation: NamedLocation, number?: number, customMarkerType?: MAP_MARKER_TYPE.MAPLIBRE_GL_DEFAULT | CUSTOM_MAP_MARKER): void {
  const markerTypeToUse = customMarkerType || MAP_CONFIG.markerType;
  if (markerTypeToUse === MAP_MARKER_TYPE.MAPLIBRE_GL_AMPLIFY_DEFAULT) {
   // Marker from Maplibre GL Amplify Library
   if (this.mapDrawPoints) {
    this.namedLocations.push(namedLocation);
    this.mapDrawPoints.setData([...this.namedLocations]);
   } else {
    if (!this.drawPointInitEventEmitter) {
     this.drawPointInitEventEmitter = new EventEmitter();
     this.drawPointInitEventEmitter.asObservable().forEach(() => {
      this.namedLocations.push(...this.bufferDrawPoints);
      this.mapDrawPoints.setData([...this.bufferDrawPoints]);
      this.drawPointInitEventEmitter.complete();
     });
     this.bufferDrawPoints.push(namedLocation);
    }
   }
  } else if (markerTypeToUse === MAP_MARKER_TYPE.MAPLIBRE_GL_DEFAULT) {
   // Marker from MapLibre GL Library
   const marker = this.getMapGlMarker(namedLocation, number);
   this.mapMarkers.push(marker);
   marker.addTo(this.map);
  } else {
   const customMarkerVariant = (customMarkerType as CUSTOM_MAP_MARKER) || MAP_CONFIG.customMarkerVariant;
   this.getCustomMapMarkerHTML(customMarkerVariant).then(markerHtml => {
    const marker = this.getMapGlMarker(namedLocation, number, markerHtml);
    this.mapMarkers.push(marker);
    marker.addTo(this.map);
   });
  }
  this.mapBounds.extend(namedLocation.coordinates);
  if (MAP_CONFIG.getMapBoundingOptions) {
   this.map.fitBounds(this.mapBounds, MAP_CONFIG.getMapBoundingOptions());
  }
 }

 /**
  * Get MapLibre GL Marker for given Named Location, may show a number and/or use a custom HTML Marker
  * @param namedLocation Named location
  * @param number Number to sho on marker
  * @param customHtml Custom HTML for the marker
  */
 getMapGlMarker(namedLocation: NamedLocation, number?: number, customHtml?: HTMLElement): Marker {
  let marker: Marker;
  if (customHtml) {
   marker = new Marker({ element: customHtml, anchor: 'bottom' });
  } else {
   marker = new Marker({ color: MAP_CONFIG.markerColor });
  }
  marker.setLngLat(namedLocation.coordinates);
  if (number && Number.isInteger(number)) {
   const markerHTML = marker.getElement();
   markerHTML.setAttribute('data-number', number.toString());
   // console.log(markerHTML);
  }
  return marker;
 }

 /**
  * Get div element for a MapLibre Amplify marker with number
  * @param number Number to show on marker
  * @returns An HTMLElement containing elements for a MapLibre Amplify Map marker with number
  */
 getCustomMapMarkerHTML(customMarkerVariant: CUSTOM_MAP_MARKER = CUSTOM_MAP_MARKER.CUSTOM_HTML_1): Promise {
  const markerComponent = getMarkerComponent();
  const markerComponentRef = this.viewContainerRef.createComponent(markerComponent);
  const eventObserver = markerComponentRef.instance.onAfterViewChecked.asObservable();
  return new Promise(resolve => {
   eventObserver.subscribe((componentNumber: boolean) => {
    const htmlString = markerComponentRef.location.nativeElement.innerHTML as string;
    if (componentNumber) {
     markerComponentRef.instance.onAfterViewChecked.complete();
     markerComponentRef.destroy();
     const markerDiv = document.createElement('div');
     markerDiv.classList.add(getMarkerClass());
     markerDiv.innerHTML = htmlString.replace(/_ngcontent-ng-[A-z0-9]+=""\s/gi, '');
     resolve(markerDiv);
    }
   });
  });

  function getMarkerComponent():
   | typeof MapMarkerCustom1Component
   | typeof MapMarkerCustom2Component
   | typeof MapMarkerCustom3Component
   | typeof MapMarkerAmplifyMapLibreGLComponent {
   switch (customMarkerVariant) {
    case CUSTOM_MAP_MARKER.CUSTOM_HTML_1:
     return MapMarkerCustom1Component;
    case CUSTOM_MAP_MARKER.CUSTOM_HTML_2:
     return MapMarkerCustom2Component;
    case CUSTOM_MAP_MARKER.CUSTOM_HTML_3:
     return MapMarkerCustom3Component;
    case CUSTOM_MAP_MARKER.MAPLIBRE_GL_AMPLIFY:
     return MapMarkerAmplifyMapLibreGLComponent;
   }
   return MapMarkerCustom1Component;
  }

  function getMarkerClass(): string {
   return CUSTOM_MAP_MARKER_CLASSES[customMarkerVariant];
  }
 }

 /**
  * Remove all markers from the map
  */
 removeAllMapMarkers(): void {
  this.mapBounds = new LngLatBounds();
  if (MAP_CONFIG.markerType === MAP_MARKER_TYPE.MAPLIBRE_GL_AMPLIFY_DEFAULT) {
   // Remove markers added as draw points through MapLibreGL Amplify Library
   this.mapDrawPoints.setData([]);
   this.namedLocations.length = 0;
  } else {
   // Remove markers added through MapLibreGL Map APIs
   this.mapMarkers.forEach(marker => marker.remove());
   this.mapMarkers.length = 0;
  }
 }
}

This TypeScript file (map.component.ts) is responsible for defining the behavior and logic of the MapComponent. It imports necessary modules and components, including those from Angular, AWS Amplify, and Maplibre GL.

Here’s a breakdown of the key components and functionalities:

  1. Imports: The file imports various Angular modules and components, AWS Amplify modules, Maplibre GL modules, custom marker components, and environment configurations.
  2. Constants and Configuration: It defines constants for configuring the map, including marker colors, map styles, and search options for the AWS Location Service.
  3. Component Class: The MapComponent class implements the OnInit, AfterViewInit, and OnChanges interface.It initializes properties such as map settings, map markers, and location searches.
  4. Lifecycle Hooks: The component implements lifecycle hooks such as ngOnInitngAfterViewInit, and ngOnChangesto handle component initialization, view initialization, and changes in input data, respectively.
  5. Methods:
  • configureAmplify: Configures AWS Amplify with credentials and Location Service configuration.
  • initMap: Initializes the map using Maplibre GL and sets up event listeners for map interactions.
  • addMapControls: Adds map controls, such as navigation and geocoder controls.
  • initDrawPoints: Initializes draw points on the map for markers using Maplibre GL Amplify.
  • onClickAddMarker: Handles adding a marker to the map when the map is clicked.
  • handleAddPredefinedLocationClick: Handles adding predefined locations to the map.
  • searchByAddress: Searches for a place using AWS Location Service based on the provided address.
  • addLocationMarkerInMap: Adds a location marker to the map based on the provided named location and marker type.
  • getMapGlMarker: Creates a Maplibre GL marker for the specified named location, number, and custom HTML.
  • getCustomMapMarkerHTML: Retrieves the HTML for a custom map marker based on the specified variant.
  • removeAllMapMarkers: Removes all markers from the map.

Overall, this file encapsulates the functionality required to display and interact with a map using Maplibre GL in an Angular application, including adding markers, searching for locations, and handling user interactions with the map.

4.06. Custom Map Marker — SVG

src/app/components/map/markers/map-marker-amplify-maplibre-gl/map-marker-amplify-maplibre-gl.component.svg

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 64 64" width="32" height="32">
<path
d="M24.8133 38.533C18.76 31.493 13 28.8264 13 20.8264C13.4827 14.9864 16.552 9.67169 21.368 6.33302C33.768 -2.26165 50.824 5.78902 52.0667 20.8264C52.0667 28.613 46.5733 31.6797 40.6533 38.373C32.4933 47.5464 35.4 63.093 32.4933 63.093C29.72 63.093 32.4933 47.5464 24.8133 38.533ZM32.4933 8.23969C26.5573 8.23969 21.7467 13.0504 21.7467 18.9864C21.7467 24.9224 26.5573 29.733 32.4933 29.733C38.4293 29.733 43.24 24.9224 43.24 18.9864C43.24 13.0504 38.4293 8.23969 32.4933 8.23969Z"
fill="#2b678c" />
<circle fill="white" cx="50%" cy="30%" r="13"></circle>
</svg>

4.07. Custom Map Marker — Logic (Typescript)

src/app/components/map/markers/map-marker-amplify-maplibre-gl/map-marker-amplify-maplibre-gl.component.ts

import { AfterViewChecked, Component, EventEmitter, Output } from '@angular/core';

@Component({
 selector: 'map-marker-amplify-maplibre-gl',
 templateUrl: './map-marker-amplify-maplibre-gl.component.svg',
 styleUrls: ['./map-marker-amplify-maplibre-gl.component.scss'],
})
export class MapMarkerAmplifyMapLibreGLComponent implements AfterViewChecked {
 @Output('onAfterViewChecked') onAfterViewChecked = new EventEmitter();
 ngAfterViewChecked(): void {
  this.onAfterViewChecked.emit(true);
 }
}

The MapMarkerAmplifyMapLibreGLComponent is an Angular component responsible for rendering a map marker using an SVG image. Here’s an overview of its structure and functionality:

SVG Template (map-marker-amplify-maplibre-gl.component.svg):

  • Defines an SVG image with a path element representing the marker’s shape and a circle element representing a highlight.
  • The path element defines the main shape of the marker, while the circle element adds a white highlight.
  • The SVG image is scalable and can be customized using CSS.

Component Class (map-marker-amplify-maplibre-gl.component.ts):

  • Defines the MapMarkerAmplifyMapLibreGLComponentclass, which:
  • It implements the AfterViewChecked lifecycle hook to emit an event after Angular checks the view.
  • It emits an onAfterViewChecked event after Angular checks the view, indicating that the SVG image has been rendered.

This component acts as a reusable map marker for Angular applications. It allows developers to easily add map markers with custom SVG images, providing flexibility in marker design and appearance. Additionally, by using Angular’s event emitter, it enables communication with parent components to perform actions based on the marker’s lifecycle events.

Similarly, there are three more custom markers based on SVG. You can find them in the GitHub repository.

At this point, all the essential code to run the application should be complete (except for CSS, which you can add as needed).

You can run the app using the following command:

npm run start

OR

ng serve
You can access it in any browser on the same machine where you ran the command using the following URL (assuming the port and IP are set to default): http://localhost:4200

5. App Screenshots:

Sign Up Page
Sign Up Page

 

OTP Verification
OTP Verification

 

Login
Login

 

Map Initial
Map Initial

 

Map with Different Markers
Map with Different Markers

 

Map Place Index Search
Map Place Index Search

Conclusion

Overall, the application demonstrates the implementation of various Angular components, including a map component, login component, button component, OTP verification dialog, and message dialog, along with custom map markers. These components enable user authentication, facilitate interaction with maps, and display messages to the user.

As a whole, the application serves as a practical guide to:

  • Implementing user sign-up and sign-in using AWS Cognito User Pool and Identity Pool
  • Authenticating users and managing their sessions
  • Getting started with AWS Location Service Maps and Place Index
  • Exploring different ways to add markers on the map
  • Adding and managing custom markers

Reference

If you’d like to jump into code now, this is the repository where you’ll find everything explained here:

https://github.com/AkshayBhanawala/Angular-AWS-Cognito-Location-Map-Markers

 

Akshay Bhanawala

+ posts
Privacy Preferences
When you visit our website, it may store information through your browser from specific services, usually in form of cookies. Here you can change your privacy preferences. Please note that blocking some types of cookies may impact your experience on our website and the services we offer.