Friday, May 28, 2010

Google Maps: Dynamically Drawn and Resizable Polygon Overlay

In extension to my previous post on Dynamically Movable and Resizable Circle Overlay, I created yet another nifty tool to dynamically draw a polygon overlay on Google Maps. You won’t find another robust tool like this for Polygon drawing on the internet. Users can draw polygon dynamically by adding, deleting and moving the polygon boundary nodes. User can press shift+click on the map to add a new node to polygon boundary or they can press ctrl+click on any node marker to remove it. The node markers can be dragged around to resize the polygon.

This is how it looks like in action:
Google Maps Dynamic Polygon

The JavaScript code is pretty easy to modify for your custom needs. The initialize() function is used to initialize the Google Map object when the page loads. A click event is also added to the map for adding node markers on the map. The click event takes care of the case when the user click on the previous markers while holding the shift key. When the user clicks on the map, addMarker() function is called to add another boundary node to the polygon. For every newly added marker, drag events are assigned for resizing the polygon and a click event is assigned to delete the node when ctrl key is pressed while clicking the marker pin. The fitPolygon() function is used to set map bounds and center to adjust the polygon inside it.

Put the following JavaScript code in the head section of your page:

<script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=YOUR_API_KEY&sensor=true" type="text/javascript"></script>
<script type="text/javascript">
/* Developed by: Abhinay Rathore [web3o.blogspot.com] */ 
//Global variables
var global = this;
var map;

var PolygonMarkers = []; //Array for Map Markers
var PolygonPoints = []; //Array for Polygon Node Markers
var bounds = new GLatLngBounds; //Polygon Bounds
var Polygon; //Polygon overlay object
var polygon_resizing = false; //To track Polygon Resizing

//Polygon Marker/Node icons
var redpin = new GIcon(); //Red Pushpin Icon
redpin.image = "http://maps.google.com/mapfiles/ms/icons/red-pushpin.png";
redpin.iconSize = new GSize(32, 32);
redpin.iconAnchor = new GPoint(10, 32);
var bluepin = new GIcon(); //Blue Pushpin Icon
bluepin.image = "http://maps.google.com/mapfiles/ms/icons/blue-pushpin.png";
bluepin.iconSize = new GSize(32, 32);
bluepin.iconAnchor = new GPoint(10, 32);

function initialize() { //Initialize Google Map
    if (GBrowserIsCompatible()) {
        map = new GMap2(document.getElementById("map_canvas")); //New GMap object
        map.setCenter(new GLatLng(38.90, -94.68007), 13);

        var ui = new GMapUIOptions(); //Map UI options
        ui.maptypes = { normal:true, satellite:true, hybrid:true, physical:false }
        ui.zoom = {scrollwheel:true, doubleclick:true};
        ui.controls = { largemapcontrol3d:true, maptypecontrol:true, scalecontrol:true };
        map.setUI(ui); //Set Map UI options

        //Add Shift+Click event to add Polygon markers
        GEvent.addListener(map, "click", function(overlay, point, overlaypoint) {
            var p = (overlaypoint) ? overlaypoint : point;
            //Add polygon marker if overlay is not an existing marker and shift key is pressed
            if (global.shiftKey && !checkPolygonMarkers(overlay)) { addMarker(p); }
        });
    }
}

// Adds a new Polygon boundary marker
function addMarker(point) {
    var markerOptions = { icon: bluepin, draggable: true };
    var marker = new GMarker(point, markerOptions);
    PolygonMarkers.push(marker); //Add marker to PolygonMarkers array
    map.addOverlay(marker); //Add marker on the map
    GEvent.addListener(marker,'dragstart',function(){ //Add drag start event
        marker.setImage(redpin.image);
        polygon_resizing = true;
    });
    GEvent.addListener(marker,'drag',function(){ drawPolygon(); }); //Add drag event
    GEvent.addListener(marker,'dragend',function(){   //Add drag end event
        marker.setImage(bluepin.image);
        polygon_resizing = false;
        drawPolygon();
        fitPolygon();
    });
    GEvent.addListener(marker,'click',function(point) { //Add Ctrl+Click event to remove marker
        if (global.ctrlKey) { removeMarker(point); }
    });
    drawPolygon();

    //If more then 2 nodes then automatically fit the polygon
    if(PolygonMarkers.length > 2) fitPolygon();
}

// Removes a Polygon boundary marker
function removeMarker(point) {
    if(PolygonMarkers.length == 1){ //Only one marker in the array
        map.removeOverlay(PolygonMarkers[0]);
        map.removeOverlay(PolygonMarkers[0]);
        PolygonMarkers = [];
        if(Polygon){map.removeOverlay(Polygon)};
    }
      else //More then one marker
      {
        var RemoveIndex = -1;
        var Remove;
        //Search for clicked Marker in PolygonMarkers Array
        for(var m=0; m<PolygonMarkers.length; m++)
        {
            if(PolygonMarkers[m].getPoint().equals(point))
            {
                RemoveIndex = m; Remove = PolygonMarkers[m]
                break;
            }
        }
        //Shift Array elemeents to left
        for(var n=RemoveIndex; n<PolygonMarkers.length-1; n++)
        {
            PolygonMarkers[n] = PolygonMarkers[n+1];
        }
        PolygonMarkers.length = PolygonMarkers.length-1 //Decrease Array length by 1
        map.removeOverlay(Remove); //Remove Marker
        drawPolygon(); //Redraw Polygon
      }
}

//Draw Polygon from the PolygonMarkers Array
function drawPolygon()
{
    PolygonPoints.length=0;
    for(var m=0; m<PolygonMarkers.length; m++)
    {
        PolygonPoints.push(PolygonMarkers[m].getPoint()); //Add Markers to PolygonPoints node array
    }
    //Add first marker in the end to close the Polygon
    PolygonPoints.push(PolygonMarkers[0].getPoint());
    if(Polygon){ map.removeOverlay(Polygon); } //Remove existing Polygon from Map
    var fillColor = (polygon_resizing) ? 'red' : 'blue'; //Set Polygon Fill Color
    Polygon = new GPolygon(PolygonPoints, '#FF0000', 2, 1, fillColor, 0.2); //New GPolygon object
    map.addOverlay(Polygon); //Add Polygon to the Map

    //TO DO: Function Call triggered after Polygon is drawn
}

//Fits the Map to Polygon bounds
function fitPolygon(){
    bounds = Polygon.getBounds();
    map.setCenter(bounds.getCenter(), map.getBoundsZoomLevel(bounds));
}
//check is the marker is a polygon boundary marker
function checkPolygonMarkers(marker) {
    var flag = false;
    for (var m = 0; m < PolygonMarkers.length; m++) {
        if (marker == PolygonMarkers[m])
        { flag = true; break; }
    }
    return flag;
}

//////////////////[ Key down event handler ]/////////////////////
//Event handler class to attach events
var EventUtil = {
      addHandler: function(element, type, handler){
            if (element.addEventListener){
                    element.addEventListener(type, handler, false);
            } else if (element.attachEvent){
                    element.attachEvent("on" + type, handler);
            } else {
                    element["on" + type] = handler;
            }
      }
};

// Attach Key down/up events to document
EventUtil.addHandler(document, "keydown", function(event){keyDownHandler(event)});
EventUtil.addHandler(document, "keyup", function(event){keyUpHandler(event)});

//Checks for shift and Ctrl key press
function keyDownHandler(e)
{
      if (!e) var e = window.event;
      var target = (!e.target) ? e.srcElement : e.target;
      if (e.keyCode == 16 && !global.shiftKey) { //Shift Key
            global.shiftKey = true;
      }
      if (e.keyCode == 17 && !global.ctrlKey) { //Ctrl Key
            global.ctrlKey = true;
      }
}
//Checks for shift and Ctrl key release
function keyUpHandler(e){
      if (!e) var e = window.event;
      if (e.keyCode == 16 && global.shiftKey) { //Shift Key
            global.shiftKey = false;
      }
      if (e.keyCode == 17 && global.ctrlKey) { //Ctrl Key
            global.ctrlKey = false;
      }
}
</script>

To initialize the map you can call the functions on page load event and include a div tag inside your body to hold the Map.

<body onload="initialize()" onunload="GUnload()">
<div id="map_canvas" style="width:100%; height:450px"></div>
</body>

Feel free to modify and use this code on your website. I have used API v2 for my code but you can easily modify this code for API v3. Happy Mapping!

Thursday, May 27, 2010

Google Maps: Dynamically Movable and Resizable Circle Overlay

Google provides a pretty wholesome Map API for creating customized maps with enhanced features. In a recent project of mine I had to use dynamically drawn map overlays for searching house properties within them. To implement that I looked for some algorithms to draw circles on Google Map Overlays and found a good one at http://koti.mbnet.fi/ojalesa/googlepages/circle.htm, but this program was built to change the radius using html form field. To take it a few steps further, I planned to make this circle movable and resizable by directly dragging map icons.

      This is one of the best dynamic circle overlay drawing tool you can find on internet. There are two pin markers to alter the circle, the Blue pin is used to drag the circle around on the map and the Red one can be dragged to resize the circle. You can even set minimum and maximum radius for the circle in the JavaScript code. The resize marker pin always stays at the 0 degree edge of the circle. The Circle fill color changes to red if the circle is moved or resized.

This is how it looks like in action: 

Google Map Dynamic Circle

JavaScript code: The logistic code is pretty easy to modify for your custom needs. The initialize() function is used to initialize the Google Map object when the page loads. Circle Center and Resize markers are then added at the default center location. Marker drag events are assigned to Center and Resize markers for dragging and resizing the circle. The drawCircle() function implements the algorithm for drawing the circle and finally the fitCircle() function is used to set the Map bounds to include the full circle inside it. If you want to trigger any other function after the circle is drawn, you can call your function after the fitCircle() function call at the end of drawCircle() function. Google Map uses Metric Units for distance by default, you will have to use conversion factor (1km = 0.621371192mi) to convert radius value into miles if you wish to.

Place the following JavaScript code in the head section of your page:
<script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=YOUR_API_KEY&sensor=true" type="text/javascript"></script>
<script type="text/javascript">
/* Developed by: Abhinay Rathore [web3o.blogspot.com] */
//Global variables
var map;
var bounds = new GLatLngBounds; //Circle Bounds
var map_center = new GLatLng(38.903843, -94.680096);

var Circle; //Circle object
var CirclePoints = []; //Circle drawing points
var CircleCenterMarker, CircleResizeMarker;
var circle_moving = false; //To track Circle moving
var circle_resizing = false; //To track Circle resizing
var radius = 1; //1 km
var min_radius = 0.5; //0.5km
var max_radius = 5; //5km

//Circle Marker/Node icons
var redpin = new GIcon(); //Red Pushpin Icon
redpin.image = "http://maps.google.com/mapfiles/ms/icons/red-pushpin.png";
redpin.iconSize = new GSize(32, 32);
redpin.iconAnchor = new GPoint(10, 32);
var bluepin = new GIcon(); //Blue Pushpin Icon
bluepin.image = "http://maps.google.com/mapfiles/ms/icons/blue-pushpin.png";
bluepin.iconSize = new GSize(32, 32);
bluepin.iconAnchor = new GPoint(10, 32);

function initialize() { //Initialize Google Map
    if (GBrowserIsCompatible()) {
        map = new GMap2(document.getElementById("map_canvas")); //New GMap object
        map.setCenter(map_center);

        var ui = new GMapUIOptions(); //Map UI options
        ui.maptypes = { normal:true, satellite:true, hybrid:true, physical:false }
        ui.zoom = {scrollwheel:true, doubleclick:true};
        ui.controls = { largemapcontrol3d:true, maptypecontrol:true, scalecontrol:true };
        map.setUI(ui); //Set Map UI options

        addCircleCenterMarker(map_center);
        addCircleResizeMarker(map_center);
        drawCircle(map_center, radius);
    }
}

// Adds Circle Center marker
function addCircleCenterMarker(point) {
    var markerOptions = { icon: bluepin, draggable: true };
    CircleCenterMarker = new GMarker(point, markerOptions);
    map.addOverlay(CircleCenterMarker); //Add marker on the map
    GEvent.addListener(CircleCenterMarker, 'dragstart', function() { //Add drag start event
        circle_moving = true;
    });
    GEvent.addListener(CircleCenterMarker, 'drag', function(point) { //Add drag event
        drawCircle(point, radius);
    });
    GEvent.addListener(CircleCenterMarker, 'dragend', function(point) { //Add drag end event
        circle_moving = false;
        drawCircle(point, radius);
    });
}

// Adds Circle Resize marker
function addCircleResizeMarker(point) {
    var resize_icon = new GIcon(redpin);
    resize_icon.maxHeight = 0;
    var markerOptions = { icon: resize_icon, draggable: true };
    CircleResizeMarker = new GMarker(point, markerOptions);
    map.addOverlay(CircleResizeMarker); //Add marker on the map
    GEvent.addListener(CircleResizeMarker, 'dragstart', function() { //Add drag start event
        circle_resizing = true;
    });
    GEvent.addListener(CircleResizeMarker, 'drag', function(point) { //Add drag event
        var new_point = new GLatLng(map_center.lat(), point.lng()); //to keep resize marker on horizontal line
        var new_radius = new_point.distanceFrom(map_center) / 1000; //calculate new radius
        if (new_radius < min_radius) new_radius = min_radius;
        if (new_radius > max_radius) new_radius = max_radius;
        drawCircle(map_center, new_radius);
    });
    GEvent.addListener(CircleResizeMarker, 'dragend', function(point) { //Add drag end event
        circle_resizing = false;
        var new_point = new GLatLng(map_center.lat(), point.lng()); //to keep resize marker on horizontal line
        var new_radius = new_point.distanceFrom(map_center) / 1000; //calculate new radius
        if (new_radius < min_radius) new_radius = min_radius;
        if (new_radius > max_radius) new_radius = max_radius;
        drawCircle(map_center, new_radius);
    });
}

//Draw Circle with given radius and center
function drawCircle(center, new_radius) {
    //Circle Drawing Algorithm from: http://koti.mbnet.fi/ojalesa/googlepages/circle.htm

    //Number of nodes to form the circle
    var nodes = new_radius * 40;
    if(new_radius < 1) nodes = 40;

    //calculating km/degree
    var latConv = center.distanceFrom(new GLatLng(center.lat() + 0.1, center.lng())) / 100;
    var lngConv = center.distanceFrom(new GLatLng(center.lat(), center.lng() + 0.1)) / 100;

    CirclePoints = [];
    var step = parseInt(360 / nodes) || 10;
    var counter = 0;
    for (var i = 0; i <= 360; i += step) {
        var cLat = center.lat() + (new_radius / latConv * Math.cos(i * Math.PI / 180));
        var cLng = center.lng() + (new_radius / lngConv * Math.sin(i * Math.PI / 180));
        var point = new GLatLng(cLat, cLng);
        CirclePoints.push(point);
        counter++;
    }
    CircleResizeMarker.setLatLng(CirclePoints[Math.floor(counter / 4)]); //place circle resize marker
    CirclePoints.push(CirclePoints[0]); //close the circle polygon
    if (Circle) { map.removeOverlay(Circle); } //Remove existing Circle from Map
    var fillColor = (circle_resizing || circle_moving) ? 'red' : 'blue'; //Set Circle Fill Color
    Circle = new GPolygon(CirclePoints, '#FF0000', 2, 1, fillColor, 0.2); //New GPolygon object for Circle
    map.addOverlay(Circle); //Add Circle Overlay on the Map
    radius = new_radius; //Set global radius
    map_center = center; //Set global map_center
    if (!circle_resizing && !circle_moving) { //Fit the circle if it is nor moving or resizing
        fitCircle();
        //Circle drawing complete trigger function goes here

    }
}

//Fits the Map to Circle bounds
function fitCircle() {
    bounds = Circle.getBounds();
    map.setCenter(bounds.getCenter(), map.getBoundsZoomLevel(bounds));
}
</script>

To initialize the map you can call the functions on page load event and include a div tag inside your body to hold the Map.

<body onload="initialize()" onunload="GUnload()">
<div id="map_canvas" style="width:100%; height:450px"></div>
</body>

Feel free to modify and use this code on your website. I have used API v2 for my code but you can easily modify this code for API v3. Happy Mapping!