Create a Nearby Places List with Google Maps in Ionic 2 – Part 1

Create a Nearby Places List with Google Maps in Ionic 2 – Part 1

Follow Josh Morony on

Maps in mobile applications are endlessly useful. One particular use might be to display a bunch of places of interests to a user – all the nearby restaurants that you operate, or maybe nearby tourist information booths, or even public toilets. Maps are great at visually representing information and can give you a geographical sense of where you need to go.

A plain old list can come in just as handy, though. If you’re busting for the toilet, then maybe you just want to know wherever the closest one is right now. In this tutorial, we will be walking through how to build a list that will sort places of interest based on the proximity to the user’s current location, and we will also include a standard map to accompany it.

Here’s what it will look like when it is done:

Ionic 2 Nearby Locations

Some key concepts we will be covering are:

  • Loading data from a JSON source
  • Displaying multiple markers in Google Maps
  • Calculating the distance between two points, and mapping that information into an array
  • Ordering data and displaying it in a list (ordered from lowest to highest)

The tutorial will be split up into two parts. In the first part, we will focus on getting the template set up along with our map. In the second part, we will work on loading some locations into the app and calculating which locations are closest to the user (as well as adding those locations to the map).

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 Project

We will start off by generating a new Ionic project with the following command:

ionic start ionic2-nearby blank --v2

Once that has finished generating you should change into the project by running the following command:

cd ionic2-nearby

We are going to require a few additional pages and providers in our application, so we are going to use the Ionic CLI to generate them now. To do that, you will need to run the following generate commands:

ionic g page Map
ionic g page List
ionic g provider Connectivity
ionic g provider GoogleMaps
ionic g provider Locations

We will get to the implementation of all of these eventually, but let me quickly go over what we will be using them for. The Map page will be used to show the Google Map with markers indicating the location of places of interest, and the List page will display the same information in list format (except that it will also list the distance to each place, and the list will be sorted in order from closest to furthest).

The GoogleMaps provider will handle setting up Google Maps in the application, and the Connectivity provider will be used in conjunction with the GoogleMaps provider to help load in the Google Maps Javascript SDK. Since I have already created a tutorial on how this works, and it is quite a long tutorial, I will be leaving most of the explanation around this out – we will mostly just copy and paste the code. If you want an explanation of how it works, I would recommend that you read Creating an Advanced Google Maps Component in Ionic 2

The Locations provider will be responsible for loading the location data into the application, and it will also handle calculating the distances from the user’s location, and ordering the data accordingly.

Before we can use all of these, we will need to set them up 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 { MapPage } from '../pages/map/map';
import { ListPage } from '../pages/list/list';
import { Locations } from '../providers/locations';
import { GoogleMaps } from '../providers/google-maps';
import { Connectivity } from '../providers/connectivity';

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

2. Create a Tabs Layout

We will be using a tabs layout for this application. We will have two tabs, which will consist of the Map page and List page. To set this up we are going to need to make a few changes to our automatically generated Home page.

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

import { Component } from '@angular/core';
import { MapPage } from '../map/map';
import { ListPage } from '../list/list';

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

  tab1Root: any = MapPage;
  tab2Root: any = ListPage;

  constructor(){

  }

}

Here we are just importing our two pages and setting up references to them. Now let’s get the template sorted.

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

<ion-tabs color="light">
  <ion-tab [root]="tab1Root" tabTitle="Map" tabIcon="navigate"></ion-tab>
  <ion-tab [root]="tab2Root" tabTitle="List" tabIcon="list"></ion-tab>
</ion-tabs>

This will handle setting up our two tabs, and if you ionic serve your application now you should be able to see these two tabs and switch between them.

3. Implement Google Maps

The next thing we are going to do is get the Google Maps implementation set up. As I mentioned before, I won’t be explaining how any of this works – if you’re interested you should take a look at my previous tutorial: Creating an Advanced Google Maps Component in Ionic 2.

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

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

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;   
    }
  }

}

This provider can be used to detect whether or not an active Internet connection is available, and it will use the best method available to detect that based on whether the app has been built with Cordova or not.

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

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

declare var google;

@Injectable()
export class GoogleMaps {

  mapElement: any;
  pleaseConnect: any;
  map: any;
  mapInitialised: boolean = false;
  mapLoaded: any;
  mapLoadedObserver: any;
  markers: 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(() => {
              resolve(true);
            });

            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) => {

        // UNCOMMENT FOR NORMAL USE
        //let latLng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);

        let latLng = new google.maps.LatLng(40.713744, -74.009056);

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

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

      });

    });

  }

  disableMap(): void {

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

  }

  enableMap(): void {

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

  }

  addConnectivityListeners(): void {

    document.addEventListener('online', () => {

      console.log("online");

      setTimeout(() => {

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

          this.enableMap();
        }

      }, 2000);

    }, false);

    document.addEventListener('offline', () => {

      console.log("offline");

      this.disableMap();

    }, false);

  }

  addMarker(lat: number, lng: number): void {

    let latLng = new google.maps.LatLng(lat, lng);

    let marker = new google.maps.Marker({
      map: this.map,
      animation: google.maps.Animation.DROP,
      position: latLng
    });

    this.markers.push(marker);  

  }

}

This handles all the logic for setting up the Google Maps Javascript SDK, and then initialising a new map in the application. There are some slight differences between the code above and the code for this provider in the full tutorial I linked. This version of the provider contains an addMarker function, which will handle adding markers to the map for us, and it also adds some functionality that will display a “Please Connect to the Internet” message if the user does not have an active Internet connection.

NOTE: In the code above we had hard coded a latitude and longitude in New York City. This is because the example locations I am using are all centered around there, but in a real world scenario, you would likely want to use the user’s actual location which can be obtained using the Geolocation plugin.

We also need to add some CSS to make sure the map displays properly.

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

page-map {

  #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%;
  }

}

This CSS is important to include – without it, you will just see an empty white space instead of your map. It also handles setting up the styles for the “Please Connect” message that we want to show.

Now we just need to add the map, by making use of our Google Maps provider, to the Map page.

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

<ion-header>
  <ion-navbar color="secondary">
    <ion-title>
      Nearby
    </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>

We add the container for the map here, and notice that we create the local references #pleaseConnect and #map which we will grab with the @ViewChild decorator in just a moment.

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

import { Component, ElementRef, ViewChild } from '@angular/core';
import { Locations } from '../../providers/locations';
import { GoogleMaps } from '../../providers/google-maps';
import { NavController, Platform } from 'ionic-angular';

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

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

  constructor(public navCtrl: NavController, public maps: GoogleMaps, public platform: Platform, public locations: Locations) {

  }

  ionViewDidLoad(){

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

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

    });

  }

}

Here we just grab a reference to the map and please connect elements, and then make a call to initialise the map using the Google Maps provider when the platform is ready.

At this stage, you should be able to see an empty Google Map loaded into your map page.

4. Set up the List

Now we are going to set up the template for the list tab. For now, we are just going to use some dummy data, but in the next part we will pull in some real (and sorted) data from our Locations provider.

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

<ion-header>

  <ion-navbar color="secondary">
    <ion-title>List</ion-title>
  </ion-navbar>

</ion-header>


<ion-content>

    <ion-list no-lines>
        <ion-item>
            <ion-avatar item-left>
                <ion-icon name="pin"></ion-icon>
            </ion-avatar>
            <h2>Some Cool Place</h2>
            <p>0.45 miles</p>
        </ion-item>
    </ion-list>

</ion-content>

5. Styling

The last thing we are going to do in this part is add a little bit of styling to the application. All we’re going to do is change the Named Color Variables to use some more interesting colours.

Modify src/theme/variables.scss to reflect the following:

$colors: (
  primary:    #1abc9c,
  secondary:  #2ecc71,
  danger:     #16a085,
  light:      #f2f6f7,
  dark:       #2c3e50
);

Summary

We’ve got most of the application set up now, all that is missing is the location data (as well as the distance calculation and sorting magic that comes along with it). If you’ve been following along up until now, your application should look something like this:

Ionic 2 Nearby Locations

In the next part, we will finish off the application entirely and explain some cool concepts along the way.

Check out my latest videos: