Advanced Tips for Using Google Maps API

At Compass, we are using the Google Maps API for searching and displaying real estate data. The key advantage of Google Maps over other map APIs is Street View, which is particularly important in real estate.

Because we’ve been using Google Maps for a few years on web and iOS, we’d like to share some best practices for using this API that will help your team build better apps that use maps. The types of problems that we’ve faced are applicable to many travel and real estate uses, as well as others that want highly interactive maps.

Research

Our first approach to using Google Maps was to see if someone else had built Angular components that wrap the Maps API, since we want to avoid reinventing the whole thing. Unfortunately, Google has not released a first-party wrapper for Google Maps with Angular, so we looked at open source options. The main contenders here are angular-google-maps and ng-map, and there are variants of these libraries for most major frontend frameworks (React, Angular 2, etc).

After researching the two libraries mentioned and looking at other usage of maps, the common set of functionality offered by existing libraries is:

  • An asynchronous way of loading the Google Maps API script
  • A component to render a map with zoom, center, and styling
  • A way to render one or more markers on the map, usually with a clickable state
  • Some extra functionality, usually wrapping additional Maps libraries like place suggestions, geocoding, or others

In broad terms, we found that it was confusing that each of these libraries wraps the Google API in its own set of APIs that are different from what the Google documentation says, which means you need to learn both APIs. It doesn’t actually simplify your work with maps, so we don’t recommend using a wrapper library around Google Maps.

Even if the design of these libraries was sound, we were not able to use a wrapper because we also require:

  • Custom buttons on top of the map
  • Street view
  • Map overlays with Angular bindings
  • Drawing and editing shapes on the map
  • Client-side usage of other Google services like Geocoding and Places Suggestions

So based on all of this, we decided to write our own wrappers, and recommend that you do too.

Creating reusable components for each map type

We use many kinds of maps at Compass, and each one is built as a separate component that can be used in a few ways. So for example, we have:

  • Styled maps with a single styled marker
  • Maps with clustered listing data
  • A static map with many markers
  • Street view

Each component has an API that is suited for the use case, with as much shared internal code as possible. We’ve found this to be the right level of abstraction, because it lets us reuse maps and related tools in different parts of our app, but also lets us quickly develop new ways of using geographic information.

Asynchronous loading of the Google Maps API

This is a feature that many libraries offer, and we have also found it to be useful. We only load the Maps API when you go to a screen that uses it, which is not everywhere in our app. This makes our app more responsive at first and makes it unit testable.

Asynchronous loading can add a lot of boilerplate code that waits for the script to load. To avoid that boilerplate, we use our router (ui-router) to also manage asynchronous script loading. For us, that looks like:

resolve: {
googleMaps: (ucGoogleMapsApiService) => {
return ucGoogleMapsApiService.load();
},
},

Two ways of adding buttons to the map

Using an absolutely-positioned element on top of the map

This is the option that is easiest to integrate into your app, where you write your UI using the same tools you would normally use, and render the elements on top of the map. We’ve chosen this option for simple “close” buttons as well as for our most complicated map feature, custom boundary search. We choose this option most of the time.

Pro:

  • Does not use Map API at all
  • Easy to add conditional logic and styling

Con:

  • Not directly integrated into the map

Integrating buttons into the map

Sometimes we want to position our UI as if it were part of the map itself, like next to the Zoom controls or satellite view controls. It takes more work to hook up all the event handling and rendering logic that the API requires, but is good for replacing default UI with your own. The way to do this is with custom controls:

Pro:

  • Interacts with built-in map controls like zoom

Con:

  • Harder to style
  • Harder to add conditional logic

Street View

When we think of street view, usually we are thinking of what happens on Google Maps when you type an address into their search bar. You get a preview of the street view pointing directly at the address you typed in. Unfortunately, the Street View API does not make it easy to have a similar type of street view experience to what you get in Google’s own apps.

Using default parameters for a street view at a particular location, you get a panorama that will face north (not toward your location) and will sometimes be an indoor panorama. Because of these defaults, our best practice is to:

  • never use the default street view request, and
  • always point the camera at your desired location from the panorama’s center

This sample code will find a panorama using our best practices, and point the camera at the location we want. It does not make the street view visible, that’s up to your app. You can use this sample code for street view images, or for interactive panoramas.

// Assumption: you have a map already
const streetView = map.getStreetView();
// Set location to be the lat/lng you want a street view for, more accurate is better
const location = map.getCenter();
const streetViewService = new maps.StreetViewService();
streetViewService.getPanorama({
// Set location to whatever you want
location,
// Usually the NEAREST panorama matches Google Maps better than the BEST panorama, aka we
// are looking for a straight-on view of a building, and BEST often returns a steeper
// angle than NEAREST
preference: maps.StreetViewPreference.NEAREST,
// We do not want indoor street view
source: maps.StreetViewSource.OUTDOOR,
}, (pano, status) => {
if (status === 'OK') {
// Pano ID is a session-stable reference to the queried street view, used for the static
// and dynamic street view lookup
const panoId = pano.location.pano;
// The default heading of street view is north, we point it to the actual location
const heading = maps.geometry.spherical.computeHeading(pano.location.latLng, location);
streetView.setPano(panoId);
streetView.setPov({
heading,
pitch: 0,
});
} else {
// Failure case, this is where you could show an error or hide street view
}
});

Custom overlays inside the map, with Angular bindings

The Maps API has limited ways of creating interactive map markers, which are used on most real estate and travel websites. Fortunately, there is a way to create Custom Overlays at any location on the map, which is how we can gain complete control of the map markers.

The Google Maps JavaScript API provides an OverlayView class for creating your own custom overlays. The OverlayView is a base class that provides several methods you must implement when creating your overlays. The class also provides a few methods that make it possible to translate between screen coordinates and locations on the map.

Like we’ve said above, our practice when developing map components is to start with the API:

<uc-map-with-clusters source-function="$ctrl.getMapData()">
<!-- Map clusters are repeated for each item from the source data -->
<uc-map-cluster item="item">
<uc-map-cluster-hover>{{item.title}}</uc-map-cluster-hover>
<uc-map-cluster-active>{{item.title}} Active state</uc-map-cluster-active>
</uc-map-cluster>
</uc-map-with-clusters>

Based on this API, we have our uc-map-with-clusters which creates the map element, and then creates each uc-map-cluster instance which will be a custom OverlayView. With Angular, this API will require usage of $compile to bind data to these DOM elements.

The most important part of this API is that we need to use a directive to detach and repeat the HTML, similar to an ng-repeat but repeating however our app chooses. Each marker will be created by calling $compile on the HTML and necessary scope.

export default function clustersDirective() {
return {
controller,
scope: true,
bindToController: {
// App-maker must configure a data loader callback
sourceFunction: '&',
},
compile(tElement) {
// Templates are detached at compile time, then cleared.
// They are rendered dynamically. Cloning them for IE-11; otherwise
// template child elements get lost.
const clusterTemplate = tElement.find('uc-map-cluster').clone();
tElement.html('');
return {
post(scope, element, attrs, [clustersCtrl]) {
clustersCtrl.template = clusterTemplate;
},
};
},
};
}

Putting all the parts together, we get a JSFiddle example with angular-compiled map markers:

https://jsfiddle.net/wylieconlon/7Lsp5ckz/

Do you want to work on interesting mapping applications?

Apply to Compass to work on our real estate technology!