Add Google Maps, Places, and Geolocation to an Ionic App

by - April 02, 2017


Introduction

You may think, do we really need yet another ionic maps tutorial? Yes we do. In this tutorial we are going to cover how to implement Google maps inside an ionic app, but in a way that goes much deeper than all the posts and tutorials you may find around the web on this topic.
We merged three of the most common cases around location based apps (mapsgeolocation and places) into one functional and coherent/comprehensive example.

Google Maps and ionic mobile apps are a perfect match. By itself, the Google Maps API is an amazing piece of tech, but when you combine it with a device that’s meant to be mobile, it opens up an array of possibilities. There’s a ton of awesome apps out there that uses Google Maps to do all kinds of marvelous things.
Even when you’re making an app that doesn’t have maps as the core functionality, it can often be quite useful as a supplementary feature as well (by displaying the location of your business on a map, for example).
In this tutorial we will cover how to integrate the Google Maps JavaScript SDK into an Ionic application.

Maps + Geolocation + Places Autocomplete + Nearby Places

We use the popular Google Maps API to display and interact with maps. There’s a ton of things you can do with the maps API and we will cover most of them in this unique ionic tutorial.
A location based app without geolocation capabilities is worthless, which is why we’ll cover this subject in the tutorial.
The last important functionality we will explore is how to apply place recommendations and suggestions using Google Places API, with its up-to-date information about millions of locations.
Combining all these functionalities, we’ll be building an app where you will be able to search for places using Google autocomplete and place suggestions, and then use the nearby search endpoint to find restaurants around your desired location.
To improve the user experience of our location app example, we will be using map markers clusters that will re-arrange when you zoom-in or zoom-out of the map.
At the end of this tutorial, you’ll learn how to build a super complete location based app with multiple interactions and functionalities.
Now, let’s get our hands on with this code!

Hands on!

Dependencies

Ad time!: If you are looking for a beautiful starter app you must have a look at our beautiful mobile themes, templates & components. Specially our Starters Category.

Step 1: First things first (maps, location and basic settings)

Let’s start with the basics. We are going to use the directives provided by ngMap to interact in an easy way with Google Maps Javascript API from angular.
You will find tons of examples showcasing the vast possibilities of this library here. We’ll show the most useful examples further in this tutorial.

The map

Let’s focus on a basic task, displaying a map.
<div class="map-container" data-tap-disabled="true">
  <ng-map class="map" zoom="14" center="[{{latitude}}, {{longitude}}]" zoom-control="true" scale-control="false" map-type-control="false" street-view-control="false" map-initialized="init(map)">
  </ng-map>
</div>
We can play with some settings to modify the controls shown in the map:
  • zoom-control: displays + and - buttons for changing the zoom level of the map.
  • scale-control: displays a map scale element.
  • map-type-control: is available in a dropdown or horizontal button bar style, allowing the user to choose a map type (ROADMAPSATELLITEHYBRID, or TERRAIN).
  • street-view-control: contains a Pegman icon which can be dragged onto the map to enable Street View.
We also set the map full width and height and you can achieve this with these useful css styles:
.map-container
{
  position: absolute;
  top: 0px;
  bottom: 0px;
  left: 0px;
  right: 0px;

  .map
  {
    height: 100%;
    width: 100%;
  }
}
Setting the map full width and height, while removing some controls from the map, makes it look much more clean and simple.
I’m not saying those controls are useless, but in a small screen you should consider before adding more elements to the screen in order to avoid a saturated and cluttered user experience.

Custom Markers

Sometimes it’s useful to add a custom callout inside a map. You can do this with custom markers.
<ng-map class="map" zoom="14" center="[{{latitude}}, {{longitude}}]" zoom-control="true" scale-control="false" map-type-control="false" street-view-control="false" map-initialized="init(map)">
  <custom-marker ng-repeat="cm in customMarkers" position="[{{cm.lat}}, {{cm.lng}}]" class="{{cm.class}}">
    <h3 class="marker-title">{{cm.text}}</h3>
  </custom-marker>
</ng-map>

Getting the user location into the game

Let’s use the capabilities of our phone and get the current geo location position and center the map around the users actual location.
<div class="row geolocation-row">
  <div class="col col-center">
    <button class="geolocation-button button button-block button-positive" ng-click="tryGeoLocation()">
      Try the Geo Location feature!
    </button>
  </div>
</div>
After adding the button, let’s code the functionality. For this task we are going to use ngCordova and the cordova-plugin-geolocation to access the device GPS data.
$scope.tryGeoLocation = function(){
  $ionicLoading.show({
    template: 'Getting current position ...'
  });

  // Clean map
  cleanMap();
  $scope.search.input = "";

  $cordovaGeolocation.getCurrentPosition({
    timeout: 10000,
    enableHighAccuracy: true
  }).then(function(position){
    $ionicLoading.hide().then(function(){
      $scope.latitude = position.coords.latitude;
      $scope.longitude = position.coords.longitude;

      createMarker({lat: position.coords.latitude, lng: position.coords.longitude});
    });
  });
};
We also added a loading message using ionic $ionicLoading to improve the user experience while we load the current location.
After these basic examples, this is how it should look like:

Step 2: The extra mile (places autocomplete, nearby places and more)

Let’s dig deeper, and add advanced functionalities to our app in a coherent way.

Search places with autocomplete capabilities

We are going to add a search bar with an autocomplete to search for places.
<div class="search-container">
  <div class="list list-inset search-input-outer">
    <label class="item item-input search-box">
      <i class="icon ion-ios-search placeholder-icon"></i>
      <input ng-model="search.input" type="text" placeholder="Search restaurants near place" ng-change="getPlacePredictions(search.input)"/>
    </label>
  </div>
  <div ng-show="predictions.length > 0" class="row row-no-padding search-results">
    <div class="col">
      <div class="list location-results-list">
        <a ng-repeat="prediction in predictions" class="item item-icon-left location-item" ng-click="selectSearchResult(prediction)">
          <i class="icon ion-location"></i>
          <h3 class="location-name">{{prediction.terms[0].value}}</h3>
          <span class="location-desc">{{prediction.description}}</span>
        </a>
      </div>
    </div>
  </div>
</div>
$scope.getPlacePredictions = function(query){
  if(query !== "")
  {
    GooglePlacesService.getPlacePredictions(query)
    .then(function(predictions){
      $scope.predictions = predictions;
    });
  }else{
    $scope.predictions = [];
  }
};
Notice that we are not using the places-auto-complete directive from ngMap as it renders the search results (predictions) in a way that is incompatible with ionic.
This is why we followed the handcrafted way and layout the list of predictions ourselves.
The magic happens in our services.js, where we ping Google Autocomplete Services to get predictions for the autocompletion feature.
this.getPlacePredictions = function(query){
  var dfd = $q.defer(),
    service = new google.maps.places.AutocompleteService();

  service.getPlacePredictions({ input: query }, function(predictions, status){
    if (status != google.maps.places.PlacesServiceStatus.OK) {
      dfd.resolve([]);
    }
    else
    {
      dfd.resolve(predictions);
    }
  });

  return dfd.promise;
};
Once the user finds the place he was looking for with the autocomplete widget, we are going to search what’s nearby that place.

What’s nearby?

That’s quite impressive, but what if you could search what’s near the place you are going?
Google has us covered, but we need to get the location (latitude and longitude) from the autocomplete result you selected.
By default, we don’t get this data from the autocomplete search, we only get what they call a place ID.
That’s something like a unique identifier for that place within Google giant places database. You can find more information about the reverse geocoding by place ID in that link.
$scope.selectSearchResult = function(result){
  $scope.search.input = result.description;
  $scope.predictions = [];

  $ionicLoading.show({
    template: 'Searching restaurants near '+result.description+' ...'
  });

  // With this result we should find restaurants arround this place and then show them in the map
  // First we need to get LatLng from the place ID
  GooglePlacesService.getLatLng(result.place_id)
  .then(function(result_location){
    // Now we are able to search restaurants near this location
    GooglePlacesService.getPlacesNearby(result_location)
    .then(function(nearby_places){
      // Clean map
      cleanMap();

      $ionicLoading.hide().then(function(){
        // Create a location bound to center the map based on the results
        var bound = new google.maps.LatLngBounds(),
            places_markers = [];

        for (var i = 0; i < nearby_places.length; i++) {
        bound.extend(nearby_places[i].geometry.location);
        var place_marker = createMarker(nearby_places[i]);
          places_markers.push(place_marker);
      }

        // Create cluster with places
        createCluster(places_markers);

        var neraby_places_bound_center = bound.getCenter();

        // Center map based on the bound arround nearby places
        $scope.latitude = neraby_places_bound_center.lat();
        $scope.longitude = neraby_places_bound_center.lng();

        // To fit map with places
        $scope.mymap.fitBounds(bound);
      });
    });
  });
};
And here are the calls to the Google API …
One to get the location from the place
this.getLatLng = function(placeId){
  var dfd = $q.defer(),
   geocoder = new google.maps.Geocoder;

 geocoder.geocode({'placeId': placeId}, function(results, status) {
    if(status === 'OK'){
      if(results[0]){
    dfd.resolve(results[0].geometry.location);
   }
   else {
    dfd.reject("no results");
   }
  }
  else {
   dfd.reject("error");
  }
 });

  return dfd.promise;
};
And another to get the nearby places
this.getPlacesNearby = function(location){
 // As we are already using a map, we don't need to pass the map element to the PlacesServices (https://groups.google.com/forum/#!topic/google-maps-js-api-v3/QJ67k-ATuFg)
 var dfd = $q.defer(),
   elem = document.createElement("div"),
    service = new google.maps.places.PlacesService(elem);

 service.nearbySearch({
    location: location,
    radius: '1000',
    types: ['restaurant']
  }, function(results, status){
  if (status != google.maps.places.PlacesServiceStatus.OK) {
     dfd.resolve([]);
   }
  else {
   dfd.resolve(results);
  }
 });

  return dfd.promise;
};
You have multiple ways to customize your search for nearby places. In this example we are setting a 1000 meter radius around the selected location and we are only searching for restaurants.
In this link you can find more information about the places types you can search for.
Also, find more information about the nearby search request here.

Clustering results

One very interesting feature, is the ability to group places results in the map using Google Marker Clusterer.
This will improve considerably the user experience as we won’t saturate the screen with tons of markers. Instead, we are going to group them based on the density of places in the area.
createCluster = function(markers){
  // var markerClusterer = new MarkerClusterer($scope.mymap, markers, {
  $scope.markers_cluster = new MarkerClusterer($scope.mymap, markers, {
    styles: [
      {
        url: '../img/i1.png',
        height: 53,
        width: 52,
        textColor: '#FFF',
        textSize: 12
      },
      {
        url: '../img/i2.png',
        height: 56,
        width: 55,
        textColor: '#FFF',
        textSize: 12
      },
      {
        url: '../img/i3.png',
        height: 66,
        width: 65,
        textColor: '#FFF',
        textSize: 12
      },
      {
        url: '../img/i4.png',
        height: 78,
        width: 77,
        textColor: '#FFF',
        textSize: 12
      },
      {
        url: '../img/i5.png',
        height: 90,
        width: 89,
        textColor: '#FFF',
        textSize: 12
      }
    ],
    imagePath: '../img/i'
  });
};
In this example we are also showcasing how to define custom marker cluster images.
You can find more information about marker clustering in these two links (link 1link 2).
Look in the example above where we define bounds around the nearby places to center the map within that bound. This little detail will help us improve the user experience.
// Create a location bound to center the map based on the results
var bound = new google.maps.LatLngBounds(),
    places_markers = [];

for (var i = 0; i < nearby_places.length; i++) {
  bound.extend(nearby_places[i].geometry.location);
  var place_marker = createMarker(nearby_places[i]);
  places_markers.push(place_marker);
}

// Create cluster with places
createCluster(places_markers);

var neraby_places_bound_center = bound.getCenter();

// Center map based on the bound arround nearby places
$scope.latitude = neraby_places_bound_center.lat();
$scope.longitude = neraby_places_bound_center.lng();

// To fit map with places
$scope.mymap.fitBounds(bound);

Do you have more information about that place?

You can also get more details about a certain place. In the function we build for creating markers, we can add an event listener for the click action.
This way when users touch a marker, they will be able to get more information about that place.
marker.addListener('click', function() {
  showPlaceInfo(place);
});
Find more about binding map elements to events here.
showPlaceInfo = function(place){
  $state.go('place', {placeId: place.place_id});
}
Once the user clicks on some place, we route him to the place details page where we make the call to the Google API for extra information about the location.
.state('place', {
  url: '/place/:placeId',
  templateUrl: "views/place.html",
  controller: 'PlaceCtrl',
  resolve: {
    place: function($stateParams, GooglePlacesService) {
      return GooglePlacesService.getPlaceDetails($stateParams.placeId);
    }
  }
})
this.getPlaceDetails = function(placeId){
 // As we are already using a map, we don't need to pass the map element to the PlacesServices (https://groups.google.com/forum/#!topic/google-maps-js-api-v3/QJ67k-ATuFg)
 var dfd = $q.defer(),
   elem = document.createElement("div"),
    service = new google.maps.places.PlacesService(elem);

 service.getDetails({
    placeId: placeId
  }, function(place, status){
  if (status == google.maps.places.PlacesServiceStatus.OK) {
     dfd.resolve(place);
   }
  else {
   dfd.resolve(null);
  }
 });

  return dfd.promise;
};
Here you can find more information about the Place Details endpoint.

Step 3: Optimizations and the road ahead

Here are some details we like to include so you can optimize it while you work.

Custom markers

You may have noticed that we used a custom marker with the logo from ionic instead of the default one from Google. It’s relatively easy to change it, just set the image you want to use when creating the marker.
// Custom image for marker
var custom_marker_image = {
      url: '../img/ionic_marker.png',
      size: new google.maps.Size(30, 30),
      origin: new google.maps.Point(0, 0),
      anchor: new google.maps.Point(0, 30)
    },
    marker_options = {
      map: $scope.mymap,
      icon: custom_marker_image,
      animation: google.maps.Animation.DROP
    };

var marker = new google.maps.Marker(marker_options);

Clear unused data

Another improvement we included in this tutorial is a function to clear unused markers and clusters to improve the performance of the app.
cleanMap = function(){
  // Remove the markers from the map and from the array
  while($scope.markers_collection.length){
    $scope.markers_collection.pop().setMap(null);
  }

  // Remove clusters from the map
  if($scope.markers_cluster !== null){
    $scope.markers_cluster.clearMarkers();
  }
}

What else can I do?

Here’s a short list of other functionalities you can add to your application using Google Maps:

You May Also Like

0 comments