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:
- Bootstrap - for UI components
- Bootstrap Icons - free and open-source icon pack
- ngx-toastr - for toast notifications
- @angular/google-maps - the official Google Maps library from the Angular team
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:
- Load the JavaScript API in
index.html
- Or lazy load the API using Angular
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:
We are also going to use Bootstrap's utilities to support mobile devices. The app is going to look like this on smaller screens:
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 coordinatesgetCurrentLocation
- 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 bindingformattedAddress
- if geocoding was successful, this property will hold the human-readable addresslocationCoords
- 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:
Source Code
You can find the complete code on Github. Happy coding.