Google Maps API

Fun with MVC Objects

Luke Mahe, Google Geo Team
April 2010

Objective

Google Maps JavaScript API V3 has introduced the implementation of MVC objects that allow map objects to store state and update their presentation automatically, which are awesome, but what can you do with them?

This article presents a basic introduction to using MVC objects within V3. You will learn how to use the Maps JavaScript API V3 MVC framework to create objects that automatically respond to state changes. You will build a resizable distance widget and by the end, you'll have a greater understanding on what MVC objects are, how to use them, and why they're just so "awesome".

Before you begin, look over the API reference, and in particular the MVCObject class.

The tutorial is broken up into the following steps:

  1. Getting Started
  2. Creating the Distance Widget
  3. Binding Properties
  4. Add a Resizing Marker
  5. Making It Work
  6. Outputting Information
  7. What Can Be Done With It?

Getting Started

Start off by creating an HTML page that loads a map.

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <title>MVC is Fun</title>

    <style type="text/css">
      #map-canvas {
        height: 500px;
      }
    </style>

    <script type="text/javascript"
        src="http://www.google.com/jsapi?autoload={'modules':[{name:'maps',version:3,other_params:'sensor=false'}]}"></script>
    <script type="text/javascript">
      function init() {
        var mapDiv = document.getElementById('map-canvas');
        var map = new google.maps.Map(mapDiv, {
          center: new google.maps.LatLng(37.790234970864, -122.39031314844),
          zoom: 8,
          mapTypeId: google.maps.MapTypeId.ROADMAP
        });
      }

      google.maps.event.addDomListener(window, 'load', init);
    </script>
  </head>
  <body>
    <div id="map-canvas"></div>
  </body>
</html>

A brief explanation of the code above (step1).

  1. Add a DOCTYPE at the top of the page, in this case a HTML5 DOCTYPE. This instruction tells the browser how to render the page
  2. Create a style section and set the height of the map to '500px'. It is not necessary to include a width because default display property of a <div> element is 'block' which means it will take up as much room that is available
  3. Add a script tag to include the google maps api code
  4. Add another script tag that contains the basics of loading a map. The key part here is the event listener that calls the function init() once the page has loaded. This is a nice habit to get into rather than using techniques like onload because it allows the separation of JavaScript and HTML code and also allows other functions to attach to the same event.

Creating the Distance Widget

The next step is to create the Distance widget. This widget is essentially a circle, a marker in the center of the circle and another marker on the circumference. When the center marker is dragged, it will also move the circle and the circumference marker. When the circumference marker is dragged, the circle's radius either grows or shrinks based on whether the marker is pulled away or towards the center. Simple!

Firstly create the Distance widget constructor that will take one parameter 'map' which is a google.maps.Map class. The widget will also subclass the google.maps.MVCObject instance and that is done by setting the object's prototype to MVCObject.

Then create a draggable marker, put it on the center of the map and use MVC techniques to bind properties.

/**
 * A distance widget that will display a circle that can be resized and will
 * provide the radius in km.
 *
 * @param {google.maps.Map} map The map on which to attach the distance widget.
 *
 * @constructor
 */
function DistanceWidget(map) {
  this.set('map', map);
  this.set('position', map.getCenter());

  var marker = new google.maps.Marker({
    draggable: true,
    title: 'Move me!'
  });

  // Bind the marker map property to the DistanceWidget map property
  marker.bindTo('map', this);

  // Bind the marker position property to the DistanceWidget position
  // property
  marker.bindTo('position', this);
}
DistanceWidget.prototype = new google.maps.MVCObject();

Add to the init() function some code to create a new DistanceWidget.

var distanceWidget = new DistanceWidget(map);

The important part to take away is the two bindTo() methods that were used. By binding the marker's map property to the DistanceWidget, it means that the values are tied together. So if the map is changed by using distanceWidget.set('map', myOtherMap); then the DistanceWidget will change to the new map without having to add any extra code. Pretty cool isn't it?

This might be confusing at the start, but the simplest way to think about it is this. Given there are two objects, A and B, bound on the same property, if object A updates the bound property, then object B's property will be updated to reflect the new value of object A and vice versa.

Now you should have a map with a marker that you can drag around (step2). While dragging, the MVC object is updating the DistanceWidget's position property with the marker's position property. It may not seem like much, but it's a start! In the next section you will create a RadiusWidget which will display a circle.

Binding Properties

Create another widget called RadiusWidget that will be responsible for displaying a circle and eventually will be resizable. This step is quite similar to the one above but instead of creating a marker, create a circle.

Add the following code to your JavaScript section:

/**
 * A radius widget that add a circle to a map and centers on a marker.
 *
 * @constructor
 */
function RadiusWidget() {
  var circle = new google.maps.Circle({
    strokeWeight: 2
  });

  // Set the distance property value, default to 50km.
  this.set('distance', 50);

  // Bind the RadiusWidget bounds property to the circle bounds property.
  this.bindTo('bounds', circle);

  // Bind the circle center to the RadiusWidget center property
  circle.bindTo('center', this);

  // Bind the circle map to the RadiusWidget map
  circle.bindTo('map', this);

  // Bind the circle radius property to the RadiusWidget radius property
  circle.bindTo('radius', this);
}
RadiusWidget.prototype = new google.maps.MVCObject();


/**
 * Update the radius when the distance has changed.
 */
RadiusWidget.prototype.distance_changed = function() {
  this.set('radius', this.get('distance') * 1000);
};

Then add this code to the DistanceWidget constructor to create the RadiusWidget.

// Create a new radius widget
var radiusWidget = new RadiusWidget();

// Bind the radiusWidget map to the DistanceWidget map
radiusWidget.bindTo('map', this);

// Bind the radiusWidget center to the DistanceWidget position
radiusWidget.bindTo('center', this, 'position');

By looking at the code you can see a few things going on:

  1. The RadiusWidget is bound to the DistanceWidget's map property
  2. The RadiusWidget has bound it's center property to the DistanceWidget's position property
  3. The RadiusWidget sets a property distance on itself
  4. The circle is bound to the the RadiusWidget's center, map and radius properties

This step introduces using the MVC property changed methodology, in this case the distance_changed() function. This is a magic function that will be called whenever the distance property is set(). The distance_changed() function is updating the circle's radius property.

Because the widget is using kilometers as its measurement but the circle uses meters, this function will convert and set the radius property of the circle. If you always wanted to work in meters you could bind the radius directly to the distance property and not have the distance_changed() function at all.

At this point you have a marker and a circle and when you move the marker the circle moves with it (step3). Isn't this fun?

Add a Resizing Marker

Time to add another marker into the mix. This time add a marker to the RadiusWidget that will be used to adjust the radius of the circle. This second marker will be referred to as the 'sizer' marker.

This steps involves:

  1. Add a marker to the RadiusWidget
  2. Bind the marker's map to the RadiusWidget's map property
  3. Bind the marker's position property to a new property on the RadiusWidget called sizer_position
  4. Add a center_changed() function that will put the marker on the circle's circumference

Create a function called addSizer_() which will create a marker and bind that to the RadiusWidget's map. Add another bindTo() so that the sizer's position property and the RadiusWidget's sizer_position property are bound together. This brings up another interesting point to note about MVC. You can bind a property to any other property, if it doesn't already exist it will automatically be created.

/**
 * Add the sizer marker to the map.
 *
 * @private
 */
RadiusWidget.prototype.addSizer_ = function() {
  var sizer = new google.maps.Marker({
    draggable: true,
    title: 'Drag me!'
  });

  sizer.bindTo('map', this);
  sizer.bindTo('position', this, 'sizer_position');
};

Add this.addSizer_(); to the end of the RadiusWidget's constructor.

/**
 * Update the center of the circle and position the sizer back on the line.
 *
 * Position is bound to the DistanceWidget so this is expected to change when
 * the position of the distance widget is changed.
 */
RadiusWidget.prototype.center_changed = function() {
  var bounds = this.get('bounds');

  // Bounds might not always be set so check that it exists first.
  if (bounds) {
    var lng = bounds.getNorthEast().lng();

    // Put the sizer at center, right on the circle.
    var position = new google.maps.LatLng(this.get('center').lat(), lng);
    this.set('sizer_position', position);
  }
};

Create a function on RadiusWidget called center_changed(). This function is going to be called every time the center value is updated and will position the sizer to be on the right edge of the circle.

Check out the example (step4) and drag around the center marker. Notice how the sizer marker is now sitting on the edge of the circle and moving when you drag? Pretty fancy isn't it?

Making It Work

In the next section you will determine the distance between the center marker and the sizer marker whenever the sizer marker is dragged, then set the circle's radius to this distance between the two markers. This technique involves the following:

  1. Add a function that calculates the distance between two google.maps.LatLng locations
  2. Add a function that sets the distance of the circle based on the distance between the two markers
  3. Add an event listener to the sizer marker's drag event to set the radius of the circle
  4. Bind the DistanceWidget to the RadiusWidget's distance and bounds properties

Add the following code:

/**
 * Calculates the distance between two latlng locations in km.
 * @see http://www.movable-type.co.uk/scripts/latlong.html
 *
 * @param {google.maps.LatLng} p1 The first lat lng point.
 * @param {google.maps.LatLng} p2 The second lat lng point.
 * @return {number} The distance between the two points in km.
 * @private
*/
RadiusWidget.prototype.distanceBetweenPoints_ = function(p1, p2) {
  if (!p1 || !p2) {
    return 0;
  }

  var R = 6371; // Radius of the Earth in km
  var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
  var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
  var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
    Math.sin(dLon / 2) * Math.sin(dLon / 2);
  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  var d = R * c;
  return d;
};


/**
 * Set the distance of the circle based on the position of the sizer.
 */
RadiusWidget.prototype.setDistance = function() {
  // As the sizer is being dragged, its position changes.  Because the
  // RadiusWidget's sizer_position is bound to the sizer's position, it will
  // change as well.
  var pos = this.get('sizer_position');
  var center = this.get('center');
  var distance = this.distanceBetweenPoints_(center, pos);

  // Set the distance property for any objects that are bound to it
  this.set('distance', distance);
};

The function distanceBetweenPoints_() was added which will return in kilometers the distance between two google.maps.LatLng locations. For more information on this function and others like it, check out http://www.movable-type.co.uk/scripts/latlong.html. Next create a function called setDistance_ which sets the distance property of the RadiusWidget based on the distance between the two markers.

Then in the addSizer_ function attach a listener to the sizer marker's drag event.

var me = this;
google.maps.event.addListener(sizer, 'drag', function() {
  // Set the circle distance (radius)
  me.setDistance();
});

Lastly in the DistanceWidget's constructor, bind to the RadiusWidget's distance and bounds properties.

// Bind to the radiusWidgets' distance property
this.bindTo('distance', radiusWidget);

// Bind to the radiusWidgets' bounds property
this.bindTo('bounds', radiusWidget);

Now you have a center marker that when dragged also moves the circle and the sizer marker. You also have a sizer marker that when dragged resizes the circle (step5).

Outputting Information

The last part demonstrates how MVC properties also act like events. In the context of the DistanceWidget, you can add event listeners to the properties that you care about. In this case, add a listener to the distance_changed and position_changed events which will call a function that will display the values.

At the end of the init function add:

google.maps.event.addListener(distanceWidget, 'distance_changed', function() {
  displayInfo(distanceWidget);
});

google.maps.event.addListener(distanceWidget, 'position_changed', function() {
  displayInfo(distanceWidget);
});

Also add a new function displayInfo()

function displayInfo(widget) {
  var info = document.getElementById('info');
  info.innerHTML = 'Position: ' + widget.get('position') + ', distance: ' +
    widget.get('distance');
}

Finally add a <div> with the id 'info' after the map <div> so you have a place to display the info.

<div id="info"></div>

Now as you drag around the center marker (step6), you'll see the position value change and as you resize the circle, you'll see the distance change.

What Can Be Done With It?

If you wanted to, you could hook it up to your store locator from the Creating a Store Locator with PHP, MySQL & Google Maps article or you can connect it to another service like Twitter who has a nice API that will let you search for tweets within a given distance of a LatLng location (read more). You can see the finished result of connecting to the Twitter API.

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.