Create Marker Clusters with Google Maps in Ionic 2

Create Marker Clusters with Google Maps in Ionic 2

Follow Josh Morony on

I’ve created quite a few Google Maps tutorials for Ionic 2, including the basics like integrating the Google Maps JavaScript SDK and adding markers and content windows, through to creating a complex nearby places list with Google Maps.

It’s easy enough to add markers to a Google Map – just a couple of lines of code. You could even add 100’s across a city quite easily. As the number of markers increase, though, the map can become cluttered. If you have a high number of markers and a low zoom level, all of the markers can mash into one big blob of useless markers.

This is where the concept of marker clustering comes into play, which can be achieved using the Google Maps Marker Clusterer Library:

Marker clustering will automatically merge nearby markers into a single marker that will detail how many nearby markers there are. As you zoom in, these combination markers will separate into increasingly small clusters until you are back down to single markers again. As you can see above, this can turn what would be a cluttered and hard to digest map, into one that is visually pleasing and useful.

In this tutorial, we are going to walk through how to add marker clustering to a Google Map in an Ionic 2 application. Since I have already covered it in a previous tutorial, I will not be discussing how to set up Google Maps in detail. I will supply all the code you need, but if you would like an explanation of how it all works I would recommend reading Creating an Advanced Google Maps Component in Ionic 2.

Before We Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

1. Generate a New Ionic 2 Application

We are going to start off by generating a new blank Ionic 2 application.

Run the following command to generate a new Ionic 2 application:

ionic start ionic2-maps-cluster blank --v2

Once that has finished generating, you should make it your working directory.

Run the following command:

cd ionic2-maps-cluster

We are also going to need to do a little more setting up. We will be using two custom providers to help with the basic Google Maps implementation, and we will also be using another provider to deal with the clustering. We are going to use the generate command the Ionic CLI provides to generate these for us.

Run the following commands to generate the required providers:

ionic g provider GoogleMaps
ionic g provider Connectivity
ionic g provider GoogleMapsCluster

Our Google Maps implementation is going to handle dynamically loading the standard Google Maps JavaScript SDK based on whether an Internet connection is available or not, but we are going to be using an npm package for the clusterer library. Let’s install that now.

Run the following command to install the Marker Clusterer library:

npm install node-js-marker-clusterer --save

and so that the TypeScript compiler doesn’t complain when we are trying to access Google Maps functionality, we will need to install the types for Google Maps

Run the following command to install the types for Google Maps:

npm install @types/google-maps --save --save-exact

Finally, we need to make sure that we have all of our new providers set up appropriately in the app.module.ts file.

Modify src/app/app.module.ts to reflect the following:

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { GoogleMaps } from '../providers/google-maps';
import { GoogleMapsCluster } from '../providers/google-maps-cluster';
import { Connectivity } from '../providers/connectivity';

@NgModule({
  declarations: [
    MyApp,
    HomePage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}, GoogleMaps, GoogleMapsCluster, Connectivity]
})
export class AppModule {}

With all that set up, we are ready to jump into the code now.

2. Set up a Basic Google Maps Implementation

We are going to set up the basic Google Maps implementation first. As I mentioned before, I’ve covered this in a previous tutorial so I won’t be explaining how it works here, we will just be copying and pasting code.

Modify src/providers/connectivity.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Network } from 'ionic-native';
import { Platform } from 'ionic-angular';
import { Observable } from 'rxjs/Observable';

declare var Connection;

@Injectable()
export class Connectivity {

  onDevice: boolean;

  constructor(public platform: Platform){
    this.onDevice = this.platform.is('cordova');
  }

  isOnline(): boolean {
    if(this.onDevice && Network.connection){
      return Network.connection !== Connection.NONE;
    } else {
      return navigator.onLine; 
    }
  }

  isOffline(): boolean {
    if(this.onDevice && Network.connection){
      return Network.connection === Connection.NONE;
    } else {
      return !navigator.onLine;   
    }
  }

  watchOnline(): Observable<any> {
    return Network.onConnect();
  }

  watchOffline(): Observable<any> {
    return Network.onDisconnect();
  }

}

Modify src/providers/google-maps.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Platform } from 'ionic-angular';
import { Connectivity } from './connectivity';
import { Geolocation } from 'ionic-native';

@Injectable()
export class GoogleMaps {

  mapElement: any;
  pleaseConnect: any;
  map: any;
  mapInitialised: boolean = false;
  mapLoaded: any;
  mapLoadedObserver: any;
  currentMarker: any;
  apiKey: string;

  constructor(public connectivityService: Connectivity) {

  }

  init(mapElement: any, pleaseConnect: any): Promise<any> {

    this.mapElement = mapElement;
    this.pleaseConnect = pleaseConnect;

    return this.loadGoogleMaps();

  }

  loadGoogleMaps(): Promise<any> {

    return new Promise((resolve) => {

      if(typeof google == "undefined" || typeof google.maps == "undefined"){

        console.log("Google maps JavaScript needs to be loaded.");
        this.disableMap();

        if(this.connectivityService.isOnline()){

          window['mapInit'] = () => {

            this.initMap().then((map) => {
              resolve(map);
            });

            this.enableMap();
          }

          let script = document.createElement("script");
          script.id = "googleMaps";

          if(this.apiKey){
            script.src = 'http://maps.google.com/maps/api/js?key=' + this.apiKey + '&callback=mapInit';
          } else {
            script.src = 'http://maps.google.com/maps/api/js?callback=mapInit';       
          }

          document.body.appendChild(script);  

        } 
      }
      else {

        if(this.connectivityService.isOnline()){
          this.initMap();
          this.enableMap();
        }
        else {
          this.disableMap();
        }

      }

      this.addConnectivityListeners();

    });

  }

  initMap(): Promise<any> {

    this.mapInitialised = true;

    return new Promise((resolve) => {

      Geolocation.getCurrentPosition().then((position) => {

        //let latLng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
        let latLng = new google.maps.LatLng(-31.563910, 147.154312);

        let mapOptions = {
          center: latLng,
          zoom: 2,
          mapTypeId: google.maps.MapTypeId.ROADMAP
        }

        this.map = new google.maps.Map(this.mapElement, mapOptions);
        resolve(this.map);

      });

    });

  }

  disableMap(): void {

    if(this.pleaseConnect){
      this.pleaseConnect.style.display = "block";
    }

  }

  enableMap(): void {

    if(this.pleaseConnect){
      this.pleaseConnect.style.display = "none";
    }

  }

  addConnectivityListeners(): void {

    this.connectivityService.watchOnline().subscribe(() => {

      console.log("online");

      setTimeout(() => {

        if(typeof google == "undefined" || typeof google.maps == "undefined"){
          this.loadGoogleMaps();
        } 
        else {
          if(!this.mapInitialised){
            this.initMap();
          }

          this.enableMap();
        }

      }, 2000);

    });

    this.connectivityService.watchOffline().subscribe(() => {

      console.log("offline");

      this.disableMap();

    });

  }

}

You should note that this provider will usually center the map around the user’s current location, however, I have made the following change:

//let latLng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
        let latLng = new google.maps.LatLng(-31.563910, 147.154312);

so that the map is manually set to where the markers will be. This is just for demonstration, so you will likely want to modify this in your own application.

Modify src/pages/home/home.ts to reflect the following:

import { Component, ElementRef, ViewChild } from '@angular/core';
import { Platform } from 'ionic-angular';
import { Geolocation } from 'ionic-native';
import { GoogleMaps } from '../../providers/google-maps';
import { GoogleMapsCluster } from '../../providers/google-maps-cluster';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

    @ViewChild('map') mapElement: ElementRef;
    @ViewChild('pleaseConnect') pleaseConnect: ElementRef;

    constructor(public platform: Platform, public maps: GoogleMaps, public mapCluster: GoogleMapsCluster) {

    }

    ionViewDidLoad(): void {

        this.platform.ready().then(() => {

            let mapLoaded = this.maps.init(this.mapElement.nativeElement, this.pleaseConnect.nativeElement);

        });

    }

}

Modify src/pages/home/home.html to reflect the following:

<ion-header>
    <ion-navbar color="danger">
      <ion-title>
        Map Clusters
      </ion-title>
    </ion-navbar>
</ion-header>

<ion-content>

    <div #pleaseConnect id="please-connect">
        <p>Please connect to the Internet...</p>
    </div>

    <div #map id="map"></div>

</ion-content>

Modify src/pages/home/home.scss to reflect the following:

page-home {

  #please-connect {
    position: absolute;
    background-color: #000;
    opacity: 0.5;
    width: 100%;
    height: 100%;
    z-index: 1;
  }

  #please-connect p {
      color: #fff;
      font-weight: bold;
      text-align: center;
      position: relative;
      font-size: 1.6em;
      top: 30%;
  }

  .scroll {
      height: 100%;
  }

  #map {
      width: 100%;
      height: 100%;
  }

}

3. Implement the Cluster Provider

We should have a basic implementation of the map working now, so we can just focus on getting our marker clustering working. We’ve already generated the provider for it, so let’s walk through the implementation of it now.

Modify src/providers/google-maps-cluster.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import * as MarkerClusterer from 'node-js-marker-clusterer';
import 'rxjs/add/operator/map';

@Injectable()
export class GoogleMapsCluster {

    markerCluster: any;
    locations: any;

    constructor(public http: Http) {
        console.log('Hello GoogleMapsCluster Provider');

        this.locations = [
            {lat: -31.563910, lng: 147.154312},
            {lat: -33.718234, lng: 150.363181},
            {lat: -33.727111, lng: 150.371124},
            {lat: -33.848588, lng: 151.209834},
            {lat: -33.851702, lng: 151.216968},
            {lat: -34.671264, lng: 150.863657},
            {lat: -35.304724, lng: 148.662905},
            {lat: -36.817685, lng: 175.699196},
            {lat: -36.828611, lng: 175.790222},
            {lat: -37.750000, lng: 145.116667},
            {lat: -37.759859, lng: 145.128708},
            {lat: -37.765015, lng: 145.133858},
            {lat: -37.770104, lng: 145.143299},
            {lat: -37.773700, lng: 145.145187},
            {lat: -37.774785, lng: 145.137978},
            {lat: -37.819616, lng: 144.968119},
            {lat: -38.330766, lng: 144.695692},
            {lat: -39.927193, lng: 175.053218},
            {lat: -41.330162, lng: 174.865694},
            {lat: -42.734358, lng: 147.439506},
            {lat: -42.734358, lng: 147.501315},
            {lat: -42.735258, lng: 147.438000},
            {lat: -43.999792, lng: 170.463352}
        ];
    }

    addCluster(map){

        if(google.maps){

            //Convert locations into array of markers
            let markers = this.locations.map((location) => {
                return new google.maps.Marker({
                    position: location,
                    label: "Hello!"
                });
            });

            this.markerCluster = new MarkerClusterer(map, markers, {imagePath: 'assets/m'});

        } else {
            console.warn('Google maps needs to be loaded before adding a cluster');
        }

    }

}

This is a reasonably simple provider. In our constructor we are just setting up a bunch of locations using their latitude and longitude – later we convert all of these into Google Maps markers by using the map method on the array. If you are not sure how mapping works, you can take a look at this tutorial or the video I created about filtering, mapping, and reducing arrays (it is very useful to know).

Then we just have a single function called addCluster which takes in a Google Map as a parameter. It creates those markers from the locations array and then creates a new MarkerClusterer object. We need to supply the map we want to add the clusters to, the markers we want to add, and the images we want to use for the cluster icons.

You will need to add the m1.png, m2.png, m3.png, m4.png, and m5.png images to the assets folder in your project in order for this to work. You can either grab them from the source code for this tutorial below, or directly from the library on GitHub.

4. Trigger the Cluster Provider

All we have left to do now is trigger that addCluster function and supply it the map. If you take a look at home.ts now you will see that we have the following code:

let mapLoaded = this.maps.init(this.mapElement.nativeElement, this.pleaseConnect.nativeElement);

This calls the init function on the Google Maps service which handles setting up our basic map, but it also returns a Promise that resolves with a reference to the map that was loaded (once it has finished loading). So we can add a handler to this that will then pass that map into our cluster provider.

Modify the mapLoaded variable in src/pages/home/home.ts to reflect the following:

let mapLoaded = this.maps.init(this.mapElement.nativeElement, this.pleaseConnect.nativeElement).then((map) => {
                this.mapCluster.addCluster(map);
            });

Now once the map has finished loading, it will be passed into the addCluster function in the cluster provider and it will add the cluster to the map for us. If you take a look at the application now, it should look something like this:

Summary

The hardest part of this tutorial is mostly getting the basic implementation of the map set up, once we get by that then the map clusterer itself is pretty simple – give it a map and an array of markers and it handles the rest.

Check out my latest videos: