Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Location Queries With Firebase GeoFire and Angular Google Maps (AGM)

Episode 42 written by Jeff Delaney
full courses for pro members

Querying data based on its proximity to a user’s GPS location is critical for location-driven apps.

In this lesson, we are going to use Firebase GeoFire to make GPS location queries in Angular, then display the results using the Google Maps JavaScript API with some help from the Angular Google Maps package. The end result is a feature that will (1) determine the GPS location of the user and (2) display the location of datapoints within a certain radius of the user’s location.

Prerequisites

This tutorial assumes you already have Firebase and AngularFire2 configured in an Angular 2+ project. If you need to get up to speed, follow the initial setup guide in the AngularFire2 docs.

Setting Up Google Maps in Angular

Let’s get this lesson started by building a basic Google Map with the user’s current location.

Installing Angular Google Maps (AGM)

We will be taking advantage of Angular Google Maps (AGM) - a package designed to help us build Google Maps quickly in Angular. It wraps common Google Maps elements into Angular components and directives, along with the ability to customize their properties and events. In this example, we will be using AGM to handle the map itself, location markers, and popup windows.

npm install @agm/core --save

Obtain your Google Maps API Key

Log into the Google Cloud Platform Console, then click on APIs and Services. From here, click on Enable APIs and Services and select the Google Maps JavaScript API. Once enabled, you can retrieve the API key from the credentials page.

enable the google maps api from the gcp console

In Angular, it’s a good practice to keep this API key in an environment file.

environment.ts

export const environment = {
production: false,
firebaseConfig: {
/// Firebase Config
},

googleMapsKey: 'YOUR_GMAPS_API_KEY'

};

app.module.ts

Now we can add AGM to the app module with the Google Maps API key.


// ...omitted

import { AgmCoreModule } from '@agm/core';

@NgModule({
declarations: [
AppComponent,
],
imports: [
// ...omitted

AngularFireModule.initializeApp(firebaseConfig),
AgmCoreModule.forRoot({
apiKey: environment.googleMapsKey
})
],
bootstrap: [
AppComponent
],
})
export class AppModule { }

Build a Basic Map with AGM

using angular google maps to build a basic interactive map

Let’s start by building a component that will display the map.

ng generate component google-map

google-map.component.html

AGM’s built-in components make it easy to flesh out a map in the HTML. To start, we will center the map on the user’s current location, then add a marker to their exact GPS coordinates. We will also add a popup window that appears when the marker is clicked. The agm-info-window uses Angular transclusion to allow custom HTML to be nested inside of it.

<div *ngIf="lat && lng">

<agm-map [latitude]="lat" [longitude]="lng">

<agm-marker [latitude]="lat" [longitude]="lng">

<agm-info-window>
<h3><strong>Howdy!</strong></h3>
<p>You are here!</p>
</agm-info-window>

</agm-marker>

</agm-map>

<div>

google-map.component.ts

In the component, we can obtain the user’s current location using the global navigator.geolocation object - learn more. This data is then used to define lat and lng variables on the component.

import { Component, OnInit } from '@angular/core';

@Component({
selector: 'google-map',
templateUrl: './google-map.component.html',
styleUrls: ['./google-map.component.scss']
})
export class GoogleMapComponent implements OnInit {

lat: number;
lng: number;


constructor() { }

ngOnInit() {
this.getUserLocation()

}

private getUserLocation() {
/// locate the user
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(position => {
this.lat = position.coords.latitude;
this.lng = position.coords.longitude;

});
}
}
}

Now we have a solid starting map to build upon with GeoFire in the next section.

Working with GeoFire

First, install GeoFire via NPM.

npm install geofire --save

The main advantages of working with Firebase GeoFire include its ability to:

  • Query items within a specific geographic radius
  • Calculate distance between points
  • Set GPS coordinates in a standardized format

adding geofire data to angular google maps

geo.service.ts

All GeoFire query and update logic will be handled in a service.

ng generate service geo --module app

A GeoFire query will return the key, coordinates, and distance from a given starting point. Imagine you have a database filled with coffee shop locations in San Francisco. If you query all coffee shops within 100km of a given point, GeoFire returns the keys for the coffee shops within this radius, as well as their distance from the starting point.

In this example, I am saving the GeoFire query data in a BehaviorSubject so it can be observed and updated throughout the app.

import { Injectable } from '@angular/core';
import { AngularFireDatabase } from 'angularfire2/database';

import * as GeoFire from "geofire";
import { BehaviorSubject } from 'rxjs/BehaviorSubject'

@Injectable()
export class GeoService {

dbRef: any;
geoFire: any;

hits = new BehaviorSubject([])

constructor(private db: AngularFireDatabase) {
/// Reference database location for GeoFire
this.dbRef = this.db.list('/locations');
this.geoFire = new GeoFire(this.dbRef.$ref);
}

/// Adds GeoFire data to database
setLocation(key:string, coords: Array<number>) {
this.geoFire.set(key, coords)
.then(_ => console.log('location updated'))
.catch(err => console.log(err))
}


/// Queries database for nearby locations
/// Maps results to the hits BehaviorSubject
getLocations(radius: number, coords: Array<number>) {
this.geoFire.query({
center: coords,
radius: radius
})
.on('key_entered', (key, location, distance) => {
let hit = {
location: location,
distance: distance
}

let currentHits = this.hits.value
currentHits.push(hit)
this.hits.next(currentHits)
})
}

}

Seeding the Database

To demonstrate GeoFire, I am seeding the database with some dummy GPS coordinates. Here’s a simple script you can use to seed your database with some initial data.

private seedDatabase() {
let dummyPoints = [
[37.9, -122.1],
[38.7, -122.2],
[38.1, -122.3],
[38.3, -122.0],
[38.7, -122.1]
]

dummyPoints.forEach((val, idx) => {
let name = `dummy-location-${idx}`
console.log(idx)
this.geo.setLocation(name, val)
})
}

Displaying GeoFire Data in Google Maps

Now we can use our current user’s location in the component as the starting point for making a query. Once we have the current user’s location, we can run the GeoFire query that will update the markers.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { GeoService } from '../geo.service'

@Component({
selector: 'google-map',
templateUrl: './google-map.component.html',
styleUrls: ['./google-map.component.scss']
})
export class GoogleMapComponent implements OnInit, OnDestroy {

lat: number;
lng: number;

markers: any;
subscription: any;

constructor(private geo: GeoService) { }

ngOnInit() {
this.getUserLocation()
this.subscription = this.geo.hits
.subscribe(hits => this.markers = hits)
}

ngOnDestroy() {
this.subscription.unsubscribe()
}

private getUserLocation() {
/// locate the user
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(position => {
this.lat = position.coords.latitude;
this.lng = position.coords.longitude;


this.geo.getLocations(100, [this.lat, this.lng])
});
}
}
}

In the component html, we simply loop over the markers with *ngFor and display the relative distance from the current user when they click the marker.

<agm-marker *ngFor="let marker of markers"
[latitude]="marker.location[0]"
[longitude]="marker.location[1]"
[iconUrl]="'https://cdn1.iconfinder.com/data/icons/designer-s-tools-1/512/Coffee-64.png'">

<agm-info-window>
<h3><strong>Location Details</strong></h3>

<p>You are {{ marker.distance }} kilometers from this point</p>
</agm-info-window>

</agm-marker>

That’s it for GeoFire with Angular Google Maps. I Let me know what you think in the comments.