Playing with Google maps API – part two: Create custom controls

Today we are going to see how to realize custom controls to add to our Google maps powered map.

The only concept that should be clear, in order to achive the goal, is what prototype is and why we must use it.

Javascript is not a really object oriented language, so we can’t really talk about classes and subclassing (using extends like in Java or Actionscript), but however we can create functions that act as constructor for new js objects, and we can inherit properties from an object to another by using the prototyping technique.

Let’s consider a simple Javascript constructor function, that can be used to create an Animal object:

function Animal(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;

    this.getInfo = function() {
        alert("Name: " + this.name + "\nAge: " + this.age + "\nSex: " + this.sex);
    }
}

To extend the Animal class with a Dog class, we have to define the Dog constructor:

function Dog(name, age, sex, barkPower) {

    Animal.apply(this, arguments);

    this.barkPower = barkPower;

    this.bark = function() {

        var barkBuffer = [this.name, " says: "];

        for (var i=0; i<this.barkPower; i++) {
            barkBuffer.push("bark! ");
        }

        alert(barkBuffer.join(""));

    }

}

and then prototyping it in this way:

Dog.prototype = new Animal();

The Animal.apply() call inside Dog, is like to use super() in Java to call the SuperClass constructor.
Now our Dog has the same methods and properties of Animal, for example we can use the getInfo() method, in addition to bark():

var laika = new Dog("Laika", 5, "F", 8);
laika.getInfo();
laika.bark();

Now, let’s put the prototyping technique in practice, in order to create a custom control for our map.
The Javascript class that represents a google map’s control is GControl, so if we want to make something of us own, we must extend that class. Furthermore we have to implements at least 2 methods: initialize() and getDefaultPosition(), in practice the GControl is to be considered an interface.
The first method, must add a DOM node (a graphic button or whatelse represents our control) to the map and return it (with return), the second must return an instance of GControlPosition which is used to place the DOM node (our control) in the right position.
Our goal is to create a custom map navigation (ie: move to north, move to south, move to east and move to west), so we will create 4 js classes that will extend GControl and implements the 2 methods. We call these classes respectively: GoUpButton, GoDownButton, GoLeftButton and GoRightButton .
Let’s see how GoUpButton is structured and how it works (others buttons will differ only for their position and click handler):

function GoUpButton(map) {

    this.map = map;
    this.button = document.createElement("a");

}

The function above, is the GoUpButton‘s constructor, and as you can see it takes one argument: map, that is a reference to the map’s instance, then it creates an <a> tag that will be our clickable button.
The next step is to extend GControl:

GoUpButton.prototype = new GControl();

Now, let’s implement the initialize() method:

GoUpButton.prototype.initialize = function() {

    var map = this.map;

    this.button.className = "custom_nav_top";
    this.button.href = "javascript:;";
    this.button.title = "Move the map to the top";
    this.button.innerHTML = "N";

    GEvent.addDomListener(this.button, "click", function() {
        map.panBy(new GSize(0, 100));
    });

    map.getContainer().appendChild(this.button);

    return this.button;

}

In this method we define the CSS class for the button, set its href attribute to “javascript:;” to ensure it won’t reload the page (is not necessary, but I feel more secure by doing it), set its title attribute and its innerHTML (in this case an N, which stands for North). Then we add an (anonymous) event listener that will be triggered when the user will click the button. This listener will invoke the map’s panBy() method, which moves the map by a specific distance by using an animation. The distance is represented by an instance of GSize, which is essentially a virtual polygon, which dimension are expressed with the two parameters width and height. So with new GSize(0, 100), we will move the map 100 pixels to the top (y axis).
The last two steps in the method are add the node to the DOM (by using map’s getContainer() method, which returns the node containing the map) and returns it.

Ok, finally there is the getDeafultPosition() implementation:

GoUpButton.prototype.getDefaultPosition = function() {

    return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(240, 5));

}

This is a really simple method that returns an instance of GControlPosition, which will define how to place the control. GControlPosition takes 2 arguments: anchor and offset, the first can be one of the following constants:

  • G_ANCHOR_TOP_RIGHT
  • G_ANCHOR_TOP_LEFT
  • G_ANCHOR_BOTTOM_RIGHT
  • G_ANCHOR_BOTTOM_LEFT

that will anchor the control to the 4 possible map’s corners, the second represents the offset (ie: the distance from the anchor point). In this way we can position our button where we like (GoUpButton will be placed 5 pixel to the top of the map and 240 pixels to the left… in a map of 500×300 pixels it will appears centered on the top).
We have finished with our custom control!
Now we have to repeat the previous steps for GoDownButton, GoLeftButton and GoRightButton, by replacing the necessary values such button’s class, innerHTML, title, panBy() argument and GControlPosition() arguments.
In order to add all the 4 controls to the map we will use addControl() method:

    map.addControl(new GoUpButton(map));
    map.addControl(new GoDownButton(map));
    map.addControl(new GoLeftButton(map));
    map.addControl(new GoRightButton(map));

The result should be this:

Custom google map controls

The full code for the map above is the following:

/* go top */
function GoUpButton(map) {
    this.map = map;
    this.button = document.createElement("a");
}

GoUpButton.prototype = new GControl();

GoUpButton.prototype.initialize = function() {

    var map = this.map;

    this.button.className = "custom_nav_top";
    this.button.href = "javascript:;";
    this.button.title = "Move the map to the top";
    this.button.innerHTML = "N";

    GEvent.addDomListener(this.button, "click", function() {
        map.panBy(new GSize(0, 100));
    });

    map.getContainer().appendChild(this.button);

    return this.button;

}

GoUpButton.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(240, 5));
}

/* go down */
function GoDownButton(map) {
    this.map = map;
    this.button = document.createElement("a");
}

GoDownButton.prototype = new GControl();

GoDownButton.prototype.initialize = function() {

    var map = this.map;

    this.button.className = "custom_nav_down";
    this.button.href = "javascript:;";
    this.button.title = "Move the map to the bottom";
    this.button.innerHTML = "S";

    GEvent.addDomListener(this.button, "click", function() {
         map.panBy(new GSize(0, -100));
    });

    map.getContainer().appendChild(this.button);

    return this.button;

}

GoDownButton.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(240, 270));
}

/* go left */
function GoLeftButton(map) {
    this.map = map;
    this.button = document.createElement("a");
}

GoLeftButton.prototype = new GControl();

GoLeftButton.prototype.initialize = function() {

    var map = this.map;

    this.button.className = "custom_nav_left";
    this.button.href = "javascript:;";
    this.button.title = "Move the map to the left";
    this.button.innerHTML = "W";

    GEvent.addDomListener(this.button, "click", function() {
        map.panBy(new GSize(100, 0));
    });

    map.getContainer().appendChild(this.button);

    return this.button;

}

GoLeftButton.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(5, 140));
}

/* go right */
function GoRightButton(map) {
    this.map = map;
    this.button = document.createElement("a");
}

GoRightButton.prototype = new GControl();

GoRightButton.prototype.initialize = function() {

    var map = this.map;

    this.button.className = "custom_nav_right";
    this.button.href = "javascript:;";
    this.button.title = "Move the map to the right";
    this.button.innerHTML = "E";

    GEvent.addDomListener(this.button, "click", function() {
         map.panBy(new GSize(-100, 0));
    });

    map.getContainer().appendChild(this.button);

    return this.button;

}

GoRightButton.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(470, 140));
}

function initialize() {

    if (GBrowserIsCompatible()) {

        var map = new GMap2(document.getElementById("map_canvas"));

        map.addControl(new GoUpButton(map));
        map.addControl(new GoDownButton(map));
        map.addControl(new GoLeftButton(map));
        map.addControl(new GoRightButton(map));
        map.setCenter(new GLatLng(41.87194, 12.56738), 5);

    }

}

2 thoughts on “Playing with Google maps API – part two: Create custom controls

  1. aa

    This is a really nice blog & tutorial. I wish I’d turned out more like a programmer after studying design! As it is, I can’t even fix the custom slider I’ve implemented here. The problem is that it starts with the knob at the top instead of detecting the zoom level and placing it accordingly…Advice?

  2. daveoncode

    Thank you “aa” :-)
    however I don’t understand your problem, because I don’t see any slider or controls in your map

Leave a Reply