blog

Software screen capture

Custom Google Maps Info Windows

by

When it comes time to relate the ephemeral world of data to the physical world, Maps are key in both enterprise and consumer applications. Whatever else you might think of it, Google Maps tends to be the default option – certainly, its the only one I’ve ever had clients ask for by name.

Even when they do ask for it specifically, though, the client generally wants to set ‘their map’ apart from the generic experience – and this isn’t as easy a task as it might be. There are a lot of areas of customization for google maps that might make for a good article, but today we’ll focus on custom info windows – those displays that pop up when you click on a marker.

And there’s one added wrinkle that might catch your interest – we need to extend a google maps api object asynchronously.

Loading and Extending Google Maps Asynchronously

You’re probably familiar with loading google maps async – you create a new script element, add the script url, and provide the api version, your api key, sensor paramater and callback as query string parameters.

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://maps.googleapis.com/maps/api/jsv=3&key=yourapikey&sensor=false&callback=init';

The callback is, of course, the key to knowing when the maps api is loaded and ready to run – and so, its also the key to knowing when the api objects are available to be extended.

In order to create our custom info window, we need to extend google.maps.OverlayView. Let’s create a simple wrapper class for maps, with an init function to be called when google maps is ready.

function GMaps(){
    // Set properties
    this.mapReady = false;
}
/**
* Note you'll need to have an object of type GMaps sitting in the global context ready to receive the init callback
* which you would append to the script as object.init
*/
GMaps.prototype.init = function(){
    this.mapReady = true;
}
// OR
/*
* This function is 'static', and could be passed to the callback param as GMaps.init
*/
GMaps.init = function(){
    // No access to 'this' here - just perform whatever startup tasks you think are necessary
}

Whether we’re using the ‘static’ function or not, the init callback will let us know we can now extend and instantiate an OverlayView.

CustomWindow

We don’t want to have to stick our custom window object inside of the init callback though – that would be ugly. So instead:

/**
     * Create a custom overlay for our window marker display, extending google.maps.OverlayView.
     * This is somewhat complicated by needing to async load the google.maps api first - thus, we
     * wrap CustomWindow into a closure, and when instantiating CustomWindow, we first execute the closure (to create
     * our CustomWindow function, now properly extending the newly loaded google.maps.OverlayView), and then
     * instantiate said function.
     * Note that this version uses jQuery.
     * @type {Function}
     */
(function(){
    var CustomWindow = function(){
        this.container = $('<div class="map-info-window"></div>');
        this.layer = null;
        this.marker = null;
        this.position = null;
    };
    /**
     * Inherit from OverlayView
     * @type {google.maps.OverlayView}
     */
    CustomWindow.prototype = new google.maps.OverlayView();
    /**
     * Called when this overlay is set to a map via this.setMap. Get the appropriate map pane
     * to add the window to, append the container, bind to close element.
     * @see CustomWindow.open
     */
    CustomWindow.prototype.onAdd = function(){
        this.layer = $(this.getPanes().floatPane);
        this.layer.append(this.container);
        this.container.find('.map-info-close').on('click', _.bind(function(){
            // Close info window on click
            this.close();
        }, this));
    };
    /**
     * Called after onAdd, and every time the map is moved, zoomed, or anything else that
     * would effect positions, to redraw this overlay.
     */
    CustomWindow.prototype.draw = function(){
        var markerIcon = this.marker.getIcon(),
            cHeight = this.container.outerHeight() + markerIcon.scaledSize.height + 10,
            cWidth = this.container.width() / 2 + markerIcon.scaledSize.width / 2;
        this.position = this.getProjection().fromLatLngToDivPixel(this.marker.getPosition());
        this.container.css({
            'top':this.position.y - cHeight,
            'left':this.position.x - cWidth
        });
    };
    /**
     * Called when this overlay has its map set to null.
     * @see CustomWindow.close
     */
    CustomWindow.prototype.onRemove = function(){
        this.container.remove();
    };
    /**
     * Sets the contents of this overlay.
     * @param {string} html
     */
    CustomWindow.prototype.setContent = function(html){
        this.container.html(html);
    };
    /**
     * Sets the map and relevant marker for this overlay.
     * @param {google.maps.Map} map
     * @param {google.maps.Marker} marker
     */
    CustomWindow.prototype.open = function(map, marker){
        this.marker = marker;
        this.setMap(map);
    };
    /**
     * Close this overlay by setting its map to null.
     */
    CustomWindow.prototype.close = function(){
        this.setMap(null);
    };
    return CustomWindow;
});

We create a closure that contains our custom window object, and extend OverlayView within. You’ll notice this isn’t executing – we don’t want to call it immediately, but rather wait until init. We’ll assume we’ve assigned this function to a property of GMaps, for example:

GMaps.CustomWindow = (function(){
...

There’s a few ways we could now create our custom info window, depending on whether we decided to use an instantiated object or a ‘static’ function as our init callback. We could use a listener (listening for an event triggered on some guaranteed-present element, like ‘body’) for the static, so that we access to this when we create the info window; or, if we already have our GMaps object, we could simply instantiate it in our init function itself.

function GMaps(){
    // Set properties
    this.mapReady = false;
    $('body').one('gmaps:ready', function(){
        this.mapReady = true;
        this.infoWindow = (GMaps.CustomWindow())();
    }.bind(this));
}
GMaps.init = function(){
    $('body').trigger('gmaps:ready');
}
// OR
GMaps.prototype.init = function(){
    this.mapReady = true;
    this.infoWindow = (GMaps.CustomWindow())();
}

Content and Styling

There are some assumptions built in to the object – namely, that we want a div with the class map-info-window feel free to change this however you please. There’s one other element addressed by class name, map-info-close this is our close button, obviously.

The expected markup for our info window looks something like:

<div class="map-info-window">
    <div class="map-info-close">x</div>
    <!-- The rest of your content -->
</div>

You can insert arbitrary html content into this div (via setContent), just as you would with a standard infoWindow.
This content can then be styled as per usual.

The absolute minimum required css looks like:

.map-info-window{
    overflow:hidden;
    position:absolute;
}
.map-info-window .map-info-close{
    float:right;
    cursor:pointer;
}

In order to get your info window to look something like the top-image example, you’ll want a little more than the barebones:

.map-info-window{
    background:#333;
    border-radius:4px;
    box-shadow:8px 8px 16px #222;
    color:#fff;
    max-width:200px;
    max-height:300px;
    text-align:center;
    padding:5px 20px 10px;
    overflow:hidden;
    position:absolute;
    text-transform:uppercase;
}
.map-info-window .map-info-close{
    float:right;
    cursor:pointer;
    margin-right:-5px;
    margin-left:5px;
}
.map-info-window h5{
    font-weight:bold;
}
.map-info-window p{
    color:#939393;
}

Of course, the info window itself and all content can be styled however you please.

Marker Prerequisites

There are some prerequisites to your marker setup, however – namely, you need to specify an icon, with the scaledSize set appropriately. You can still use the generic google maps icon image, if so desired:

var marker = new google.maps.Marker({
    icon:{
        url:http://maps.google.com/mapfiles/ms/icons/red-dot.png,
        size:new google.maps.Size(32, 32),
        scaledSize:new google.maps.Size(32, 32)
    };
});

Calling the Custom Window

This works exactly the same as calling the standard info window. Assuming our infoWindow is living in GMaps as this.infoWindow, and we’re inside the GMaps ‘class’:

google.maps.event.addListener(marker, 'click', function(){
    this.infoWindow.setContent('your html content');
    this.infoWindow.open(map, marker);
}.bind(this));

jQuery Free

This isn’t a jQuery plugin, so if we want to do without jQuery, we certainly can:

/**
 * Create a custom overlay for our window marker display, extending google.maps.OverlayView.
 * This is somewhat complicated by needing to async load the google.maps api first - thus, we
 * wrap CustomWindow into a closure, and when instantiating CustomNativeWindow, we first execute the closure
 * (to create our CustomNativeWindow function, now properly extending the newly loaded google.maps.OverlayView),
 * and then instantiate said function.
 * @type {Function}
 * @see _mapView.onRender
 */
(function(){
    var CustomWindow = function(){
        this.container = document.createElement('div');
        this.container.classList.add('map-info-window');
        this.layer = null;
        this.marker = null;
        this.position = null;
    };
    /**
     * Inherit from OverlayView
     * @type {google.maps.OverlayView}
     */
    CustomWindow.prototype = new google.maps.OverlayView();
    /**
     * Called when this overlay is set to a map via this.setMap. Get the appropriate map pane
     * to add the window to, append the container, bind to close element.
     * @see CustomWindow.open
     */
    CustomWindow.prototype.onAdd = function(){
        this.layer = this.getPanes().floatPane;
        this.layer.appendChild(this.container);
        this.container.getElementsByClassName('map-info-close')[0].addEventListener('click', function(){
            // Close info window on click
            this.close();
        }.bind(this), false);
    };
    /**
     * Called after onAdd, and every time the map is moved, zoomed, or anything else that
     * would effect positions, to redraw this overlay.
     */
    CustomWindow.prototype.draw = function(){
        var markerIcon = this.marker.getIcon(),
            cBounds = this.container.getBoundingClientRect(),
            cHeight = cBounds.height + markerIcon.scaledSize.height + 10,
            cWidth = cBounds.width / 2;
        this.position = this.getProjection().fromLatLngToDivPixel(this.marker.getPosition());
        this.container.style.top = this.position.y - cHeight+'px';
        this.container.style.left = this.position.x - cWidth+'px';
    };
    /**
     * Called when this overlay has its map set to null.
     * @see CustomWindow.close
     */
    CustomWindow.prototype.onRemove = function(){
        this.layer.removeChild(this.container);
    };
    /**
     * Sets the contents of this overlay.
     * @param {string} html
     */
    CustomWindow.prototype.setContent = function(html){
        this.container.innerHTML = html;
    };
    /**
     * Sets the map and relevant marker for this overlay.
     * @param {google.maps.Map} map
     * @param {google.maps.Marker} marker
     */
    CustomWindow.prototype.open = function(map, marker){
        this.marker = marker;
        this.setMap(map);
    };
    /**
     * Close this overlay by setting its map to null.
     */
    CustomWindow.prototype.close = function(){
        this.setMap(null);
    };
    return CustomWindow;
});

That’s it.

+ more

Accurate Timing

Accurate Timing

In many tasks we need to do something at given intervals of time. The most obvious ways may not give you the best results. Time? Meh. The most basic tasks that don't have what you might call CPU-scale time requirements can be handled with the usual language and...

read more