Sunday, June 30, 2013

Using Google Maps Drawing Manager to Create User Selectable Areas

Drawing-Manager-Selections

The Google Maps API has been on my list of things to learn for some time now. I recently had an opportunity to dive in and try a few features out. There's a lot of good documentation and small demos available from Google which makes it pretty easy to get started. I decided to create a simple scenario where, given a set of places marked on the map, the user can select a region of interest and list more details about the points inside the selected area.

I started this experiment with the Drawing Tools demo, tweaking it, and adding various different features. There are several pieces needed to make it work:


  1. Create the map and center it

  2. Plot all the points the user can select

  3. Create the DrawingManager, tell it what to draw, and then listen for it to complete drawing that object

  4. Each time a selection is made, find the points inside the area and list them next to the map

  5. Maintain only one selection area. Clear the existing one in drawing more and, as a convenience, make a completed selection draggable/resizable



From that list of goals, I start digging through the documentation and examples looking for what I needed to realize the functionality. Creating the map and DrawingManager was already handled in the demo I started from. However, I didn't want to draw multiple overlays - just a circle. I tweaked the configuration options to only draw circles and not show any controls:



drawingManager = new google.maps.drawing.DrawingManager({

drawingMode: google.maps.drawing.OverlayType.CIRCLE,
drawingControl: false,

circleOptions: {
fillColor: '#ffff00',
fillOpacity: 0.3,
strokeWeight: 1,
clickable: false,
editable: false,
zIndex: 1
}

});



Now, I could only draw circles but there was no limit on how many I could draw. The next step was to listen for when a draw operation completed and track the circle object that was drawn so I could ensure only one was drawn at a time:



google.maps.event.addListener(drawingManager, 'circlecomplete', function( circle ) {
selectedArea = circle;
});



Once I capture the selected region, I know there is something selected. However, I still need to either remove the current circle before drawing a new one or prevent a new circle from being drawn. I attempted the former by listening to the map's click event:



google.maps.event.addListener(map, 'click', function() {

if ( selectedArea ) {
selectedArea.setMap(null);
google.maps.event.clearInstanceListeners(selectedArea);
}

selectedArea = null;
});


But that handler was never called. It seems the DrawingManager was preventing map click events. My solution was to use jQuery to listen for mousedown events on the map container DIV with the same handler function as above:



$('#map-canvas').on('mousedown', function() {
...
});


Now, as the next circle is drawn, the current one is removed from the map.

Before anything can be selected, there needs to be something to select. I created a simple array of points and added them to the map:


var sites = [
{ location: 'Alfond Swimming Pool', lat: 28.5903, lng: -81.3484},
{ location: 'Cahall Sandspur Field', lat: 28.5928, lng: -81.35},
...
];

function plotMarkers () {

$.each( sites, function () {

if ( this.marker ) this.marker.setMap(null);

this.position = new google.maps.LatLng(this.lat, this.lng);

this.marker = new google.maps.Marker({
position: this.position,
map: map,
title: this.location
});

});
}


I saved the LatLng object since I'll need that later to determine if a location is inside the selected area. That process is setup in the DrawingManager's circlecomplete event handler, I'm going to add a function to determine what points fall inside the circle:



google.maps.event.addListener(drawingManager, 'circlecomplete', function( circle ) {

selectedArea = circle;

listSelected();

});



That function uses the Geometry helper function computeDistanceBetween() to take the center of the circle and find the distance to each site on the map to see if its less than the radius of the circle:


function listSelected () {

var r = selectedArea.getRadius(),
c = selectedArea.getCenter();

var inside = $.map( sites, function ( s ) {

var d;

if ( ( (d = google.maps.geometry.spherical.computeDistanceBetween( s.position, c )) <= r ) )
return s.location + ' ('+(Math.round(d/100)/10)+' km)';

});

$('#map-selected').html( inside.sort().join('<br/>') );
}


If it is within the circle, it will be added to the list of locations to display next to the map. As this is a proof of concept, I've made no attempt to make this work with larger sets of data. Clearly, a different strategy would be required to accommodate anything more than a few hundred points. Also, the "extra" information is not exactly that spectacular but it shows the idea.

Right now the map is stuck in edit mode so you can't interact with the it. Every action results in a drawing activity. You can't drag the map around or use the mouse wheel to zoom (just the controls on the map). For now, I decided to toggle the map into interact/select mode by using a button to enable the DrawingManager for creating a selection. Upon drawing the circle, the DrawingManager is disabled and the resulting circle is allowed to be moved and resized. A few changes were needed to enable this feature. First, the circleOptions in the DrawingManager needed to be set:


drawingManager = new google.maps.drawing.DrawingManager({

drawingMode: google.maps.drawing.OverlayType.CIRCLE,
drawingControl: false,

circleOptions: {
fillColor: '#ffff00',
fillOpacity: 0.3,
strokeWeight: 1,
clickable: false,
editable: true,
zIndex: 1
}

});


Setting editable to true allows the circle to be modified after being drawn. Next, I added the button and toggling logic:



$('#map-controls').children().button().click(toggleSelector);

function toggleSelector () {

var $el = $('#map-controls button');

if ( $el.button('option', 'label') == 'Select' ) {

$el.button('option', 'label', 'Interact');
drawingManager.setMap(map);
} else {

$el.button('option', 'label', 'Select');
drawingManager.setMap(null);
}

selecting = !selecting;
}


And added the toggleSelector() function to the circlecomplete event handler so the DrawingManager is disabled after a circle is drawn:


google.maps.event.addListener(drawingManager, 'circlecomplete', function( circle ) {

selectedArea = circle;

google.maps.event.addListener(circle, 'center_changed', listSelected);
google.maps.event.addListener(circle, 'radius_changed', listSelected);

listSelected();
toggleSelector();

});


Additionally, I added listeners to the circle to watch for changes to the circle which may affect the selected region. When that happens, I want listSelected() to be called to rebuild the list of locations inside the selected area.

A working demo is on my sandbox along with complete source. Although relatively simple, the example examines several different features of the Goggle Maps API and how they can be tied together to create an interactive user experience.