Using Google Geocoding API in Angular

Fri Mar 26, 2021

No comments 9 reads

Angular / Tutorials / Google Maps

armin-zia

Using Google Geocoding API in Angular

In a previous post (Working with Google Maps in Angular) I showed you how to use Google Maps in Angular projects. In this post, we'll have a look at Google's Geocoding API. We'll build a simple app that supports geocoding and reverse geocoding.

What Is Geocoding?

Geocoding is the process of converting addresses (like "1600 Amphitheatre Parkway, Mountain View, CA") into geographic coordinates (like latitude 37.423021 and longitude -122.083739), which you can use to place markers on a map, or position the map.

Reverse geocoding is the process of converting geographic coordinates into a human-readable address.

The Google Geocoding API is intended for website and mobile developers who want to use geocoding data within maps provided by one of the Google Maps Platform APIs. Make sure to check out the official documentation to learn more.

Getting Started

I'm assuming you have Node.js, NPM, and Angular CLI installed. I'm using the following versions:

  • node v.12.19.0
  • npm 6.14.8
  • Angular CLI v11.2.5

Let's start by creating a new project:

ng new ng-google-geocoding-example --style=scss --routing=false

This will create a new project named ng-google-geocoding-example with SCSS styling and no routing to keep things simple. We're going to need a few libraries:

npm install --save bootstrap bootstrap-icons ngx-toastr @angular/google-maps

We're also going to need the typing definitions for Google Maps:

npm install --save-dev @types/googlemaps

Great, we have set up our project. If you run npm start the app should work without an error. But in case you got errors about the google namespace not being found, make sure to update the TypeScript compiler options. Open tsconfig.app.json in the root directory, and add the types collection under compilerOptions:

"compilerOptions": {
  // ...
  "typeRoots": ["node_modules/@types"],
  "types": ["googlemaps"]
}

Another issue you may run into is the TypeScript compiler throwing warnings about property initialization. To fix that, open tsconfig.json in the root directory and disable the strictPropertyInitialization setting:

"compilerOptions": {
  // ...
  "strictPropertyInitialization": false,
}

Next, we'll import Bootstrap and Bootstrap Icons. Open src/styles.scss and add the following:

@import "~bootstrap/scss/bootstrap";
@import "~bootstrap-icons/font/bootstrap-icons";

I prefer importing the Bootstrap SCSS because it's easier to override variables and styles this way. The second line imports the CSS file for Bootstrap Icons' web font. Alternatively, you could import them in the angular.json file:

"styles": [
  "node_modules/bootstrap/dist/css/bootstrap.min.css",
  "node_modules/bootstrap-icons/font/bootstrap-icons.css",
  "src/styles.scss"
]

Note that Bootstrap is imported before styles.scss, otherwise, your overrides won't take effect.

Loading the Google Maps API

Before you continue, you need to follow these steps to grab an API key that can be used to load Google Maps. It's worth mentioning that as of June 11th, 2018 it is now mandatory to have a billing account for your API keys to work properly. Once you add the billing information, Google gives you about $200 worth of credit. Plus, if you stay below the free rate limits (about 5000 requests per month) you won't be charged.

Once you have your API key you can load Google Maps in two different ways:

To keep things simple, I'm going to use the first approach. Open index.html and replace the content with the following markup:

<!doctype html>
<html lang="en">
<head>
  <base href="/">
  <title>Google Geocoding with Angular</title>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <div class="container">
    <main role="main" class="mt-5">
      <app-root>loading...</app-root>
    </main>
  </div>

  <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY" async defer></script>
</body>
</html>

Replace YOUR_API_KEY with the API key you have created on Google. Now let's import the required modules into our app. Open src/app/app.module.ts and update the code with the following:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { GoogleMapsModule } from '@angular/google-maps';
import { ToastrModule } from 'ngx-toastr';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    HttpClientModule,
    GoogleMapsModule,
    ToastrModule.forRoot({
      preventDuplicates: false,
      progressBar: true,
      countDuplicates: true,
      extendedTimeOut: 3000,
      positionClass: 'toast-bottom-right',
    }),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

These modules are required for this sample application to work:

  • BrowserAnimationsModule
  • FormsModule
  • HttpClientModule
  • GoogleMapsModule
  • ToastrModule

The last thing is to add your Google API key to the environment file so that we can access it later from TypeScript code. Open src/environments/environment.ts and add the following property:

export const environment = {
  production: false,
  googleApiKey: 'YOUR_API_KEY',
};

Replace YOUR_API_KEY with your API key. You need to repeat this step for the environment.prod.ts file too since Angular is going to look for it in production.

The App Component

We're going to build a simple UI with these blocks:

  • An input control for entering an address
  • Two buttons, one for getting the current location, and another for geocoding the address
  • Two panels for displaying the map, and the geocoding result

This is how our app is going to look like:

Geocoding app on desktop

We are also going to use Bootstrap's utilities to support mobile devices. The app is going to look like this on smaller screens:

Geocoding app on mobile

Open src/app/app.component.html and replace the content with the following markup:

<div class="row">
  <div class="col col-md-8 offset-md-2">
    <div class="row">
      <div class="col pr-0">
        <input type="search" class="form-control" [(ngModel)]="address" (keydown.enter)="findAddress()" placeholder="Enter an address here" [disabled]="isWorking">
      </div>
      <div class="col-auto ml-auto">
        <button class="btn btn-primary mr-1" (click)="findAddress()" [disabled]="isWorking || !address?.length">
          <i class="bi bi-map mr-md-1"></i> <span class="d-none d-md-inline-block">Find address</span>
        </button>
        <button class="btn btn-primary" (click)="getCurrentLocation()" [disabled]="isWorking">
          <i class="bi bi-geo mr-md-1"></i> <span class="d-none d-md-inline-block">Find me</span>
        </button>
      </div>
    </div>
    
    <hr>
    
    <div class="row">
      <div class="col-12 col-md-6 pr-md-0">
        <div id="info" class="shadow-sm rounded border p-3 h-100">
          <h5>Formatted Address</h5>
          <p>
            {{ formattedAddress?.length ? formattedAddress : 'N/A' }}
          </p>
          <h5>Coordinates</h5>
          <p class="mb-0">
            Latitude: {{ locationCoords ? locationCoords?.lat() : 'N/A' }}<br>
            Longitude: {{ locationCoords ? locationCoords?.lng() : 'N/A' }}
          </p>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="google_map_container shadow-sm rounded mt-3 mt-md-0">
          <google-map [zoom]="mapZoom" [center]="mapCenter" [options]="mapOptions">
            <map-marker
              #marker="mapMarker"
              [position]="mapCenter"
              [options]="markerOptions"
              (mapClick)="openInfoWindow(marker)"
              (mapDragend)="onMapDragEnd($event)">
            </map-marker>
            <map-info-window>{{ markerInfoContent }}</map-info-window>
          </google-map>
        </div> 
      </div>
    </div>
  </div>
</div>

Assuming you're familiar with Bootstrap and the @angular/google-maps library, the snipper above should be self-explanatory. But I'll go through the code quickly. The app component wraps the UI in a container:

<div class="row">
  <div class="col col-md-8 offset-md-2">
    <!-- UI components go here -->
  </div>
</div>

We have a row and a column offset by 2, on medium-sized screens. Inside this column, we have two separate rows, one for the input controls at the top, and the other for the map and geocoding information. The input controls' row consists of an input element and two buttons:

<div class="row">
  <div class="col pr-0">
    <input type="search" class="form-control" [(ngModel)]="address" (keydown.enter)="findAddress()" placeholder="Enter an address here" [disabled]="isWorking">
  </div>
  <div class="col-auto ml-auto">
    <button class="btn btn-primary mr-1" (click)="findAddress()" [disabled]="isWorking || !address?.length">
      <i class="bi bi-map mr-md-1"></i> <span class="d-none d-md-inline-block">Find address</span>
    </button>
    <button class="btn btn-primary" (click)="getCurrentLocation()" [disabled]="isWorking">
      <i class="bi bi-geo mr-md-1"></i> <span class="d-none d-md-inline-block">Find me</span>
    </button>
  </div>
</div>

The input is bound to the address property, which we'll get to in a minute. We have also wired up the (keydown.enter) event, so when you press the enter/return key the findAddress() method is called. As you can see in the markup, we're going to have two methods:

  • findAddress - geocodes an address and converts it to geographical coordinates
  • getCurrentLocation - gets your current position and reverse geocodes the coordinates into a human-readable address

The input element and the buttons get disabled while the isWorking property is true. The Find Address button will also be disabled if the input element is empty.

The second row displays the geocoding result and the actual Google Map.

<div class="row">
  <div class="col-12 col-md-6 pr-md-0">
    <div id="info" class="shadow-sm rounded border p-3 h-100">
      <h5>Formatted Address</h5>
      <p>
        {{ formattedAddress?.length ? formattedAddress : 'N/A' }}
      </p>
      <h5>Coordinates</h5>
      <p class="mb-0">
        Latitude: {{ locationCoords ? locationCoords?.lat() : 'N/A' }}<br>
        Longitude: {{ locationCoords ? locationCoords?.lng() : 'N/A' }}
      </p>
    </div>
  </div>
  <div class="col-12 col-md-6">
    <div class="google_map_container shadow-sm rounded mt-3 mt-md-0">
      <google-map [zoom]="mapZoom" [center]="mapCenter" [options]="mapOptions">
        <map-marker
          #marker="mapMarker"
          [position]="mapCenter"
          [options]="markerOptions"
          (mapClick)="openInfoWindow(marker)"
          (mapDragend)="onMapDragEnd($event)">
        </map-marker>
        <map-info-window>{{ markerInfoContent }}</map-info-window>
      </google-map>
    </div> 
  </div>
</div>

There are a number of things to bring to your attention here. The info block is straightforward, it displays the geocoding result, or N/A if you haven't clicked any of the buttons yet. The map component is bound to a number of properties, similar to what we've seen in the previous post, but there's an addition here. The (mapDragend) event handler. What we want to achieve is to have the map marker be draggable. So that we can move it around and geocode the new position. You'll also notice the <map-info-window> component which is used to open an InfoWindow when the marker is clicked. We're going to use the formatted address returned by Google to set the popup content.

The Geocoding Service

Before we complete the AppComponent, we need to create an Angular service for working with the Geocoding API. First, we need a model class that represents the response returned from Google. under src/app create a folder named models, and create the geocoder-response.model.ts file with the following code:

export class GeocoderResponse {
  status: string;
  error_message: string;
  results: google.maps.GeocoderResult[];

  constructor(status: string, results: google.maps.GeocoderResult[]) {
    this.status = status;
    this.results = results;
  }
}

Whenever you send a geocoding request to Google, a response with the above structure is returned which you can use to update your UI, call your database, etc.

Next, under src/app create another folder named services, and create the geocoding.service.ts file with the following code:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { GeocoderResponse } from '../models/geocoder-response.model';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class GeocodingService {
  constructor(private http: HttpClient) {}

  geocodeLatLng(location: google.maps.LatLngLiteral): Promise<GeocoderResponse> {
    let geocoder = new google.maps.Geocoder();

    return new Promise((resolve, reject) => {
      geocoder.geocode({ 'location': location }, (results, status) => {
        const response = new GeocoderResponse(status, results);
        resolve(response);
      });
    });
  }

  getLocation(term: string): Observable<GeocoderResponse> {
    const url = `https://maps.google.com/maps/api/geocode/json?address=${term}&sensor=false&key=${environment.googleApiKey}`;
    return this.http.get<GeocoderResponse>(url);
  }
}

Let's see what this service class is doing. We have 2 methods with the following signatures:

geocodeLatLng(location: google.maps.LatLngLiteral): Promise<GeocoderResponse>;
getLocation(term: string): Observable<GeocoderResponse>;

The geocodeLatLng method accepts a LatLngLiteral as the argument (geographical coordinates) and converts it to a human-readable address:

geocodeLatLng(location: google.maps.LatLngLiteral): Promise<GeocoderResponse> {
  let geocoder = new google.maps.Geocoder();

  return new Promise((resolve, reject) => {
    geocoder.geocode({ 'location': location }, (results, status) => {
      const response = new GeocoderResponse(status, results);
      resolve(response);
    });
  });
}

First, we create an instance of a google.maps.Geocoder and create a Promise. I haven't implemented the reject handler, but you should. Then we call the geocode method on the instance, passing in the coordinates. If successful, we create a response object using the model we just created and resolve the promise.

The getLocation method accepts a string (could be an address, place ID, postal code, etc.) and reverse geocodes it to geographical coordinates.

getLocation(term: string): Observable<GeocoderResponse> {
  const url = `https://maps.google.com/maps/api/geocode/json?address=${term}&sensor=false&key=${environment.googleApiKey}`;
  return this.http.get<GeocoderResponse>(url);
}

Here we create a URL string pointing to the Geocoding API's endpoint. We set the address parameter to the string we got from the input control, and we set the key parameter to our API key. Finally, we send an HTTP GET request to Google and return the response, which is going to be an instance of the GeocoderResponse model.

Now that we have the response model and the geocoding service in place, we can complete the app component and wire up the UI. Open src/app/app.component.ts and replace the content with the following. I'm going to walk you through the code step by step. Let's start by importing everything and defining the properties we're going to need.

import { HttpErrorResponse } from '@angular/common/http';
import { Component, ViewChild } from '@angular/core';
import { GoogleMap, MapInfoWindow, MapMarker } from '@angular/google-maps';
import { ToastrService } from 'ngx-toastr';
import { GeocoderResponse } from './models/geocoder-response.model';
import { GeocodingService } from './services/geocoding.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  constructor(
    private geocodingService: GeocodingService,
    private toastr: ToastrService
  ) {}

  @ViewChild(GoogleMap, { static: false }) map: GoogleMap;
  @ViewChild(MapInfoWindow, { static: false }) infoWindow: MapInfoWindow;

  mapZoom = 12;
  mapCenter: google.maps.LatLng;
  mapOptions: google.maps.MapOptions = {
    mapTypeId: google.maps.MapTypeId.ROADMAP,
    zoomControl: true,
    scrollwheel: false,
    disableDoubleClickZoom: true,
    maxZoom: 20,
    minZoom: 4,
  };

  markerInfoContent = '';
  markerOptions: google.maps.MarkerOptions = {
    draggable: false,
    animation: google.maps.Animation.DROP,
  };

  geocoderWorking = false;
  geolocationWorking = false;

  address: string;
  formattedAddress?: string | null = null;
  locationCoords?: google.maps.LatLng | null = null;

  get isWorking(): boolean {
    return this.geolocationWorking || this.geocoderWorking;
  }

  openInfoWindow(marker: MapMarker) {
    this.infoWindow.open(marker);
  }
}

We have injected the GeocodingService and the ToastrService in the constructor. Then we have the map and infoWindow properties using the @ViewChild directive, binding to the map and the marker components. Next, we have some properties for configuring the Google Map and the marker's InfoWindow. The important ones are the following:

  • address - this property is bound to the input control, using two-way data binding
  • formattedAddress - if geocoding was successful, this property will hold the human-readable address
  • locationCoords - the geographical coordinates on the map

You'll notice that the last two properties are nullable because initially, they won't have a value. Then we have the isWorking helper property, which returns true if the app is busy getting your current location or geocoding. The openInfoWindow doesn't need an explanation, it just opens the marker popup.

Getting the current position

Add the following method to app.component.ts:

getCurrentLocation() {
  this.geolocationWorking = true;
  navigator.geolocation.getCurrentPosition(
    (position) => {
      this.geolocationWorking = false;

      const point: google.maps.LatLngLiteral = {
        lat: position.coords.latitude,
        lng: position.coords.longitude,
      };

      this.geocoderWorking = true;
      this.geocodingService
        .geocodeLatLng(point)
        .then((response: GeocoderResponse) => {
          if (response.status === 'OK' && response.results?.length) {
            const value = response.results[0];

            this.locationCoords = new google.maps.LatLng(point);

            this.mapCenter = new google.maps.LatLng(point);
            this.map.panTo(point);

            this.address = value.formatted_address;
            this.formattedAddress = value.formatted_address;
            this.markerInfoContent = value.formatted_address;

            this.markerOptions = {
              draggable: true,
              animation: google.maps.Animation.DROP,
            };
          } else {
            this.toastr.error(response.error_message, response.status);
          }
        })
        .finally(() => {
          this.geocoderWorking = false;
        });
    },
    (error) => {
      this.geolocationWorking = false;

      if (error.PERMISSION_DENIED) {
        this.toastr.error("Couldn't get your location", 'Permission denied');
      } else if (error.POSITION_UNAVAILABLE) {
        this.toastr.error(
          "Couldn't get your location",
          'Position unavailable'
        );
      } else if (error.TIMEOUT) {
        this.toastr.error("Couldn't get your location", 'Timed out');
      } else {
        this.toastr.error(error.message, `Error: ${error.code}`);
      }
    },
    { enableHighAccuracy: true }
  );
}

If you've read my previous post on how to work with Google Maps in Angular this should look familiar since I'm using the same code base. The getCurrentLocation method uses the navigator.geolocation feature available to modern browsers to get your current position. In a production environment, you would want to check first to see if this feature is available on the browser or not. So we get the current position and if successful, we have the geographical coordinates. We pass that information to our geocoding service and get a response. If the response status is OK and the results collection is not empty, we extract the information we need and update the UI.

if (response.status === 'OK' && response.results?.length) {
  const value = response.results[0];

  this.locationCoords = new google.maps.LatLng(point);

  this.mapCenter = new google.maps.LatLng(point);
  this.map.panTo(point);

  this.address = value.formatted_address;
  this.formattedAddress = value.formatted_address;
  this.markerInfoContent = value.formatted_address;

  this.markerOptions = {
    draggable: true,
    animation: google.maps.Animation.DROP,
  };
} else {
  this.toastr.error(response.error_message, response.status);
}

The results collection is going to have more than one value and based on your requirements you may want to filter the results. Here, I'm extracting the first item which is going to be the most relevant result anyway. Next, we center the map on the coordinates and populate the address, formattedAddress and markerInfoContent properties. For this simple demo, we're only interested in the formatted address, but Google returns a lot of useful information about the coordinates (e.g. country, city, district, etc.). Pay attention to the markerOptions property, we're setting the draggable option to true because we want to be able to move the marker around and geocode the new position.

Reverse Geocoding

Now we need another method for converting an address to geographical coordinates. Add the following method to app.component.ts:

findAddress() {
  if (!this.address || this.address.length === 0) {
    return;
  }

  this.geocoderWorking = true;
  this.geocodingService
    .getLocation(this.address)
    .subscribe(
      (response: GeocoderResponse) => {
        if (response.status === 'OK' && response.results?.length) {
          const location = response.results[0];
          const loc: any = location.geometry.location;

          this.locationCoords = new google.maps.LatLng(loc.lat, loc.lng);

          this.mapCenter = location.geometry.location;

          setTimeout(() => {
            if (this.map !== undefined) {
              this.map.panTo(location.geometry.location);
            }
          }, 500);

          this.address = location.formatted_address;
          this.formattedAddress = location.formatted_address;
          this.markerInfoContent = location.formatted_address;

          this.markerOptions = {
            draggable: true,
            animation: google.maps.Animation.DROP,
          };
        } else {
          this.toastr.error(response.error_message, response.status);
        }
      },
      (err: HttpErrorResponse) => {
        console.error('geocoder error', err);
      }
    )
    .add(() => {
      this.geocoderWorking = false;
    });
}

Same process, but instead of working with a LatLng position, we have an address. We pass that to the getLocation method in our geocoding service and get the response. If successful, Google returns a collection of coordinates. Here I'm extracting the first item to create a google.maps.LatLng object. Finally, we center the map on the position and populate the address and marker properties. Notice that the call to map.panTo method is wrapped in a setTimeout call. This is to make sure the app has enough time to update before we center the map on the new coordinates.

The Drag Event Handler

The last method we need is the drag event handler. Add the following method to app.component.ts:

onMapDragEnd(event: google.maps.MouseEvent) {
  const point: google.maps.LatLngLiteral = {
    lat: event.latLng.lat(),
    lng: event.latLng.lng(),
  };

  this.geocoderWorking = true;
  this.geocodingService
    .geocodeLatLng(point)
    .then((response: GeocoderResponse) => {
      if (response.status === 'OK') {
        if (response.results.length) {
          const value = response.results[0];

          this.locationCoords = new google.maps.LatLng(point);

          this.mapCenter = new google.maps.LatLng(point);
          this.map.panTo(point);

          this.address = value.formatted_address;
          this.formattedAddress = value.formatted_address;

          this.markerOptions = {
            draggable: true,
            animation: google.maps.Animation.DROP,
          };

          this.markerInfoContent = value.formatted_address;
        }
      }
    })
    .finally(() => {
      this.geocoderWorking = false;
    });
}

When you drag the marker around the map, you get a google.maps.MouseEvent from the map component. We're creating a LatLngLiteral from the coordinates and passing it to the geocodeLatLng method in our geocoding service, similar to how we get the current position. Fabulous 🐉, that's it! You can see the app in action in the following image:

Google Geocoding Demo

Source Code

You can find the complete code on Github. Happy coding.

Updated at Fri Mar 26, 2021


Posted in Angular

Tagged Tutorials , Google Maps


Print
Armin Zia
Author

Armin Zia

Software Engineer

Leave a reply


Comment body cannot be empty 🐲