﻿window.cirrus = window.cirrus || {};
cirrus.util = cirrus.util || {};
cirrus.math = cirrus.math || {};
cirrus.web = cirrus.web || {};
cirrus.geo = cirrus.geo || {};

cirrus.util.Color = function Color() {
    //Stored as values between 0 and 1
    var red = 0;
    var green = 0;
    var blue = 0;

    this.Red = function() { return Math.round(red*255); }

    this.Green = function() { return Math.round(green*255); }

    this.Blue = function() { return Math.round(blue*255); }

    this.SetRGB = function(newR, newG, newB) {
        red = newR/255.0;
        green = newG/255.0;
        blue = newB/255.0;
    }

    this.HexString = function() {
        var rStr = this.Red().toString(16);
        if (rStr.length == 1) rStr = '0' + rStr;
        var gStr = this.Green().toString(16);
        if (gStr.length == 1) gStr = '0' + gStr;
        var bStr = this.Blue().toString(16);
        if (bStr.length == 1) bStr = '0' + bStr;
        return ('#' + rStr + gStr + bStr).toUpperCase();
    }

    this.SetHexString = function(hexString) {
        if(hexString == null || typeof(hexString) != "string") {
            this.SetRGB(0,0,0);
            return;
        }

        if (hexString.substr(0, 1) == '#')
            hexString = hexString.substr(1);

        if(hexString.length != 6) {
            this.SetRGB(0,0,0);
            return;
        }

        var r = parseInt(hexString.substr(0, 2), 16);
        var g = parseInt(hexString.substr(2, 2), 16);
        var b = parseInt(hexString.substr(4, 2), 16);
        if (isNaN(r) || isNaN(g) || isNaN(b)) {
            this.SetRGB(0,0,0);
            return;
        }

        this.SetRGB(r,g,b);
    }
}

cirrus.util.Colors = new (function() {
    this.FromHexString = function(hexString) {
        var result = new cirrus.util.Color();
        result.SetHexString(hexString);
        return result;
    };

    this.FromRgb = function(r, g, b) {
        var result = new cirrus.util.Color();
        result.SetRGB(r, g, b);
        return result;
    };
    
    this.MixColor = function(minColor, maxColor, colorKey) {
        var mixPart = function(min,max) {
            return min + (max - min) * colorKey / 15.0;
        }
        var min = cirrus.util.Colors.FromHexString(minColor);
        var max = cirrus.util.Colors.FromHexString(maxColor);
        return cirrus.util.Colors.FromRgb(
            mixPart(min.Red(), max.Red()),
            mixPart(min.Green(), max.Green()),
            mixPart(min.Blue(), max.Blue()));
    }
})();

cirrus.web.postJSON = function (url, data, success) {
    return $.post(url, data, success, 'json');
}
    
cirrus.math.getDistanceInRadians = function (p1, p2) {
    var A = degreesToRadians(Math.abs(p1.lng() - p2.lng()));
    var b = degreesToRadians(90.0 - p1.lat());
    var c = degreesToRadians(90.0 - p2.lat());
    return Math.acos(Math.cos(b) * Math.cos(c) + Math.sin(b) * Math.sin(c) * Math.cos(A));
}

cirrus.math.degreesToRadians = function (degrees) {
    return degrees * Math.PI / 180.0;
}

cirrus.math.radiansToDegrees = function (radians) {
    return radians * 180.0 / Math.PI;
}

cirrus.math.roundToXPlaces = function (value, places) {
	var multiplier = Math.pow(10, places);
	return Math.round(value * multiplier) / multiplier;
}

cirrus.geo.PolygonContainsLatLng = function (poly, latLng) {
    var path = poly.getPath();
    var numPoints = path.getLength();
    var inPoly = false;
    var j = numPoints - 1;
    
    for (var i = 0; i < numPoints; i++) {
        var vertex1 = path.getAt(i);
        var vertex2 = path.getAt(j);
        
        if ((vertex1.lng() < latLng.lng() && vertex2.lng() >= latLng.lng()) || (vertex2.lng() < latLng.lng() && vertex1.lng() >= latLng.lng())) {
            if (vertex1.lat() + (latLng.lng() - vertex1.lng()) / (vertex2.lng() - vertex1.lng()) * (vertex2.lat() - vertex1.lat()) < latLng.lat()) {
                inPoly = !inPoly;
            }
        }
        
        j = i;
    }
    
    return inPoly;
}

/**@private
* In V3 it is quite hard to gain access to Projection and Panes.
* This is a helper class
* @param {google.maps.Map} map
*/
function ProjectionHelperOverlay(map) {
    this.setMap(map);
}
ProjectionHelperOverlay.prototype = new google.maps.OverlayView();
ProjectionHelperOverlay.prototype.draw = function () {
    if (!this.ready) {
        this.ready = true;
        google.maps.event.trigger(this, 'ready');
    }
};

cirrus.geo.Demographics = function Demographics(
        map, mapCanvas, statusElement, handleCompiledData, mapLayers, activityIndicator,
        initialDataColumnId, styleSelected, styleDeselected, handleEntityIds, polygonRemovalHandler,
        mapCanvasOffset, doNotFitMapToBlockgroups) {

    var blockGroupData = null;
    var polys = new Array();
    var demographicData = null;
    var currentDataColumnId = initialDataColumnId;
    var selectedBlockGroupIds = null;
    var calculateDemographicsByArea = false;
    
    var projectionHelper = new ProjectionHelperOverlay(map);
    
    var infoDiv = document.getElementById('bgInfo');
    var isDetailPage = (infoDiv == null);
    var countyPolygonAndClickListeners = [];
    var mapClickListener = null;
    
    function AddMapCountyPolygonClickEvent()
    {
        mapClickListener = google.maps.event.addListener(map, "click", addPolyPoint);
        for (var i = 0; i < countyPolygonAndClickListeners.length; i++)
        {
            //setTimeout(function() {
                countyPolygonAndClickListeners[i].Listener = google.maps.event.addListener(countyPolygonAndClickListeners[i].Polygon, "click",addPolyPoint);
            //}, 200);
        }
    }
    
    function RemoveMapCountyPolygonClickEvent()
    {
        if (mapClickListener != null && mapClickListener != undefined){google.maps.event.removeListener(mapClickListener); }
        for (var i = 0; i < countyPolygonAndClickListeners.length; i++)
        {
            var tmp = countyPolygonAndClickListeners[i].Listener;
            if (tmp != null && tmp != undefined)
            {
            google.maps.event.removeListener(tmp);
            }
        }
    }
    
    //self calling method.
    (function GetCountyShapes() {
        cirrus.web.postJSON("censusjson.axd?counties=1", null, function(data) {
            for (var i in data.Counties) {
                var co = data.Counties[i];
                var coPoints = new google.maps.MVCArray();
                for (var j in co.Points) {
                    var p = co.Points[j];
                    coPoints.push(new google.maps.LatLng(p.Latitude, p.Longitude));
                }
                var coPoly = new google.maps.Polygon({
                    map: map,
                    paths: coPoints,
                    clickable: false,
                    fillOpacity: 0,
                    strokeOpacity: 1,
                    strokeColor: '#000000',
                    strokeWeight: 2,
                    zIndex: -1000
                });
                // we need to keep track of all the county polygons.
                // because this method is a callback from an async call we have to set the listeners up the first time.
                countyPolygonAndClickListeners.push({"Polygon" : coPoly, "Listener" : google.maps.event.addListener(coPoly,"click",addPolyPoint)});
            }
        });
    })();
    
    if (!isDetailPage) {
        google.maps.event.addListener(map, "mousemove", mapMouseMoveInfo);
        AddMapCountyPolygonClickEvent();
        
        google.maps.event.addListener(map, "dblclick", function(e) {
            if (polygonMode) {
                addPolyPoint(e);
                mapRightClick(e);
            }
        });
        google.maps.event.addDomListener(mapCanvas, "mousedown", mapMouseDown);
        google.maps.event.addListener(map, "rightclick", mapRightClick);
    }
    
    var needToClearMapLayers = true;
    
    var statusItems = {"count" : 0,"items" : new Object()};
    function SetStatus(msg) {
        statusItems.count++;
        
        statusItems.items[statusItems.count] = msg;
        $(activityIndicator).toggle(true);
        WriteStatus();
        return statusItems.count;
    }
    function RemoveStatus(id)
    {
        if (statusItems.count > 0)
        {
            statusItems.count--;
        }
        delete statusItems.items[id];
        if (statusItems.count <= 0) { $(activityIndicator).toggle(false); }
        WriteStatus();
        
    }
    function WriteStatus()
    {
        var tmp = '';
        for (var i in statusItems.items)
        {
            tmp = tmp + statusItems.items[i] + "<br/>";
        }
        statusElement.html(tmp);
    }
    
    this.toggleByArea = function() {
        calculateDemographicsByArea = !calculateDemographicsByArea;
        _GetDemoData();
        return calculateDemographicsByArea;
    }
    
    this.GetCurrentDataColumnId = function() { return currentDataColumnId; }

    this.GetBlockGroups = function(initialPoints,doNotGetResults) {
        var bounds = new google.maps.LatLngBounds();
        for (var i in initialPoints) { bounds.extend(initialPoints[i]); }
        SearchBlockGroups("rect", sprintf("%s;%s", bounds.getSouthWest().toUrlValue(), bounds.getNorthEast().toUrlValue()),null,doNotGetResults);
//        var sPoints = [];
//        for (var i in initialPoints) { sPoints.push(initialPoints[i].toUrlValue()); }
//        SearchBlockGroups("point", sPoints.join(";"));
    }
    
    this.GetDemoData = function(dataPointId){
        _GetDemoData(dataPointId,true);
    };
    
    var ajaxRequest = null;
    function GetBlockGroupsByUrl(url, postData,doNotGetResults,callback,doNotGetDemographicsData) {
        postData = postData || '';
        var statusid = SetStatus('Getting block group information...');
        if (ajaxRequest != null) 
        {
            RemoveStatus(ajaxRequest.statusid);
            ajaxRequest.abort(); 
        }
        ajaxRequest = cirrus.web.postJSON(url, postData, function(data) {
            ajaxRequest = null;
            if (data != null)
            {
                blockGroupData = data;
                selectedBlockGroupIds = data.SelectedIds;
                if (handleEntityIds != null && !doNotGetResults)
                {
                    handleEntityIds(data.EntityIds); 
                }
                RemoveStatus(statusid);
                if (!doNotGetDemographicsData)
                {
                    _GetDemoData();
                }
                DisplayBlockGroups();
                if (callback != null) { callback(); }
            }
            else
            {
                RemoveStatus(statusid);
            }
        });
        ajaxRequest.statusid = statusid;
    }

// this is not being used.    
//    function _GetBlockGroups () {
//        GetBlockGroupsByUrl('censusjson.axd');
//    }

    
    

    this.DoRadiusSearch = function (center, radiusInMiles,callback,doNotGetDemographicsData) {
        var radialPoint = CalculatePointXMilesAway(center, radiusInMiles);
        SearchBlockGroups("circle", sprintf("%s;%s", center.toUrlValue(), radialPoint.toUrlValue()),null,null,callback,doNotGetDemographicsData);
    }
    
    var EARTH_RADIUS_IN_MILES = 3958.76;
    function CalculatePointXMilesAway(point, miles) {
        var distance = miles / EARTH_RADIUS_IN_MILES; // radians
        distance = cirrus.math.radiansToDegrees(distance); // degrees
        var newLatitude = point.lat() > 0 ? point.lat() - distance : point.lat() + distance;
        return new google.maps.LatLng(newLatitude, point.lng());
    }
    
    function SearchBlockGroups(mode, points, postData,doNotGetResults,callback,doNotGetDemographicsData) {
        GetBlockGroupsByUrl('censusjson.axd?mode=' + mode + '&points=' + points, postData,doNotGetResults,callback,doNotGetDemographicsData);
    }
    
    var dataRequest = null;
    function _GetDemoData(dataPointId,skipHandleCompiledData) {
        if (null == dataPointId) { dataPointId = currentDataColumnId; }
        var statusid = SetStatus('Getting demographic information...', true);
        if (dataRequest != null) 
        {
            RemoveStatus(dataRequest.statusid);
            dataRequest.abort(); 
        }
        dataRequest = cirrus.web.postJSON('censusjson.axd?data=' + dataPointId + '&byarea=' + calculateDemographicsByArea, 'selected=' + selectedBlockGroupIds, function(data) {
            dataRequest = null;
            if (data != null)
            {
                if (data.ErrorMessage != null && data.ErrorMessage != '') {
                    //SetStatus("<pre>" + data.ErrorMessage + "</pre>");// debug
                } else {
                    RemoveStatus(statusid);
                    var tempArray = new Array();
                    for (var i in data.Values) {
                        tempArray[data.Values[i].BlockGroupId] = data.Values[i];
                    }
                    demographicData = tempArray;
                    currentDataColumnId = data.DataColumnId;
                    if (blockGroupData != null) { DisplayBlockGroups(); }
                   
                }
            }
            else
            {
                RemoveStatus(statusid);
            }
        });
        dataRequest.statusid = statusid;
        
        if (skipHandleCompiledData == null || !skipHandleCompiledData)
        {
            handleCompiledData();
        }
    }
    
    var reportRequest = null;
    this.GetReport = function(reportKey, callback, description) {
        description = description || '';
        var statusid = SetStatus('Generating report...');
        var c = callback;
        if (reportRequest != null) {
            RemoveStatus(reportRequest.statusid);
            reportRequest.abort();
        }
        reportRequest = cirrus.web.postJSON('censusjson.axd?report=' + reportKey, 'selected=' + selectedBlockGroupIds + '&desc=' + encodeURIComponent(description), function(data) {
            reportRequest = null;
            if (data != null)
            {
                if (data.ErrorMessage != null && data.ErrorMessage != '') {
                    //SetStatus("<pre>" + data.ErrorMessage + "</pre>", false);//debug
                } else {
                    c(data.Report.Content);
                    RemoveStatus(statusid);
                }
            }
            else
            {
                RemoveStatus(statusid);
            }
        });
        reportRequest.statusid = statusid;
    }
    var saveReportRequest = null;
    this.SaveReport = function(reportKey, description) {
        description = description || '';
        var statusid = SetStatus("Saving report...");
        if (saveReportRequest != null) 
        {
            RemoveStatus(saveReportRequest.statusid);
            saveReportRequest.abort(); 
        }
        saveReportRequest = cirrus.web.postJSON('censusjson.axd?save=1&report=' + reportKey, 'selected=' + selectedBlockGroupIds + '&desc=' + encodeURIComponent(description), function(data) {
            reportRequest = null;
            if (data != null)
            {
                if (data.ErrorMessage != null && data.ErrorMessage != '') {
                    //SetStatus("<pre>" + data.ErrorMessage + "</pre>");//debug
                } else {
                    RemoveStatus(statusid);
                }
            }
            else
            {
                RemoveStatus(statusid);
            }
        });
        saveReportRequest.statusid = statusid;
    }

    function GetMinColor() { return "#0000ff"; }
    function GetMaxColor() { return "#ff0000"; }

    function GetFillColor(key) {
        return cirrus.util.Colors.MixColor(GetMinColor(), GetMaxColor(), key).HexString();
    }

    function DeselectBlockGroup(id, index) {
        if (blockGroupData.BlockGroups[index].Id == id) { blockGroupData.BlockGroups.splice(index,1); }
        delete polys[id];
        selectedBlockGroupIds = selectedBlockGroupIds.replace(new RegExp('(^|,)' + id + '(,|$)'), '$1');
        _GetDemoData();
    }

    function DisplayBlockGroups() {
        var polysToHide = [];
        for (var i in polys) { polysToHide[i] = polys[i]; }
        var bounds = new google.maps.LatLngBounds();
        for (var i = 0; i < blockGroupData.BlockGroups.length; i++) {
            var bg = blockGroupData.BlockGroups[i];
            var bgPoints = new google.maps.MVCArray();
            for (var j = 0; j < bg.Points.length; j++) {
                var ll = new google.maps.LatLng(bg.Points[j].Latitude, bg.Points[j].Longitude);
                bgPoints.push(ll);
                bounds.extend(ll);
            }
            var poly = polys[bg.Id];
            var myDemoData = demographicData != null ? demographicData[bg.Id] : null;
            var fillColor = myDemoData != null ? GetFillColor(myDemoData.ColorKey) : "#909090";
            if (poly == null) {
                poly = new google.maps.Polygon({
                    fillColor: fillColor,
                    fillOpacity: 0.2,
                    map: map,
                    paths: bgPoints,
                    strokeColor: "#909090",
                    strokeWeight: 1,
                    strokeOpacity: 1,
                    clickable: !isDetailPage,
                    zIndex: 100
                });
                polys[bg.Id] = poly;
                if (!isDetailPage) {
                    google.maps.event.addListener(poly, "mouseover", function() {
                        this.setOptions({strokeWeight:3,fillOpacity:0.5});
                        SetBlockGroupInfo("<strong>" + this.InfoName + (calculateDemographicsByArea ? " (per sq.mi)" : "") + ":</strong> " + this.Information + "<br/><em>Double-click to remove</em>");
                    });
                    google.maps.event.addListener(poly, "mouseout", function() {
                        this.setOptions({strokeWeight:1,fillOpacity:0.2});
                        SetBlockGroupInfo('');
                    });
                    // The mousedown and mouseup events are here to keep the map's click event from firing when we click on a block group (if we're not in polygon-drawing mode)
                    google.maps.event.addListener(poly, "mousedown", function() {
                        if (!polygonMode) {
                            RemoveMapCountyPolygonClickEvent();
                        }
                    });
                    google.maps.event.addListener(poly, "mouseup", function() {
                        if (!polygonMode) {
                            setTimeout(function() {
                                if (mapClickListener != null) { RemoveMapCountyPolygonClickEvent(); }
                                AddMapCountyPolygonClickEvent();
                            }, 200);
                        }
                    });
                    //google.maps.event.addListener(poly, "click", function() { alert("poly click"); });
                    google.maps.event.addListener(poly, "dblclick", function(e) {
                        //alert("poly dblclick");
                        if (!polygonMode) {
                            if (pointSearchTimeout != null) { clearTimeout(pointSearchTimeout); pointSearchTimeout = null; }
                            this.setMap(null);
                            if (polygonRemovalHandler != null) { polygonRemovalHandler(this); }
                            SetBlockGroupInfo('');
                            DeselectBlockGroup(this.BlockGroupId, this.BlockGroupIndex);
                        }
                    });
                }
            } else {
                poly.setOptions({fillColor:fillColor});
                poly.setMap(map);
                delete polysToHide[bg.Id];
            }
            if (surveyDataColumns != null && surveyDataColumns[currentDataColumnId]) {
                poly.InfoName = currentDataColumnId != null ? surveyDataColumns[currentDataColumnId].Description : "Block Group ID";
            } else if (window.surveyDataColumns2) {
                if (surveyDataColumns2 != null && surveyDataColumns2[currentDataColumnId]) {
                    poly.InfoName = currentDataColumnId != null ? surveyDataColumns2[currentDataColumnId].Description : "Block Group ID";
                }
            } 
            poly.Information = myDemoData != null ? myDemoData.Value : bg.CensusId;
            poly.BlockGroupId = bg.Id;
            poly.BlockGroupIndex = i;
        }
        if (blockGroupData.BlockGroups.length > 0 && !doNotFitMapToBlockgroups) { map.fitBounds(bounds); }
        
        for (var i in polysToHide) { polysToHide[i].setMap(null); }
//        if (needToClearMapLayers) {
//            needToClearMapLayers = false;
//            for (var j in mapLayers) { mapLayers[j].setMap(null); }
//        }
    }

    function mapMouseMoveInfo(e) {
        var divPixel = projectionHelper.getProjection().fromLatLngToContainerPixel(e.latLng);
        infoDiv.style.top = (mapCanvasOffset.top + divPixel.y + 20) + "px";
        infoDiv.style.left = (mapCanvasOffset.left + divPixel.x + 10) + "px";
        if (polygonMode && myPoly != null) {
            endLatLng = e.latLng;
            drawPhantomLine();
        }
    }

    function mapMouseMoveDrawInfo(e) {
        if (rectangleMode || circleMode) {
            endLatLng = e.latLng;
            if (rectangleMode) { drawRectangle(); } else { drawCircle(); }
        }
    }

    var mapMouseUpListener = null;
    var mapMouseMoveListener = null;
    var startLatLng = null;
    var endLatLng = null;
    function mapMouseDown(e) {
        if (rectangleMode || circleMode) {
            var latLng = _getLatLngFromMouseEvent(e);
            disableHoverInfo = true;
            startLatLng = latLng;
            mapMouseUpListener = google.maps.event.addDomListener(mapCanvas, "mouseup", mapMouseUp);
            mapMouseMoveListener = google.maps.event.addListener(map, "mousemove", mapMouseMoveDrawInfo);
            RemoveMapCountyPolygonClickEvent();
        }
    }

    function mapMouseUp(e) {
        var latLng = _getLatLngFromMouseEvent(e);
        if (rectangleMode || circleMode) {
            disableHoverInfo = false;
            if (mapMouseUpListener != null) { google.maps.event.removeListener(mapMouseUpListener); mapMouseUpListener = null; }
            if (mapMouseMoveListener != null) { google.maps.event.removeListener(mapMouseMoveListener); mapMouseMoveListener = null; }
            setTimeout(function() {
                if (mapClickListener != null) { RemoveMapCountyPolygonClickEvent(); }
                AddMapCountyPolygonClickEvent();
            }, 200);
            endLatLng = latLng;
            //if (rectangleMode) { drawRectangle(); } else { drawCircle(); }
            finishDrawing();
        }
    }
    
    function mapRightClick(e) {
        destroyOverlay(phantomLine); phantomLine = null;
        finishDrawing();
    }

    this.getLatLngFromMouseEvent = _getLatLngFromMouseEvent;
    
    function _getLatLngFromMouseEvent(e) {
        var posx = 0;
        var posy = 0;
        if (!e || e == null) var e = window.event;
        if (e.pageX || e.pageY) {
            posx = e.pageX;
            posy = e.pageY;
        } else if (e.clientX || e.clientY) {
			var doc = document.documentElement, body = document.body;
			posx = e.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
			posy = e.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
        }

        var mapCoords = $(mapCanvas).offset();
        //var mapCoords = mapCanvasOffset;

        var myX = posx - mapCoords.left;
        var myY = posy - mapCoords.top;

        return projectionHelper.getProjection().fromContainerPixelToLatLng(new google.maps.Point(myX, myY));
    }

    var disableHoverInfo = false;
    function SetBlockGroupInfo(value) {
        var infoBox = $('#bgInfo');
        if (!disableHoverInfo && value != '') {
            infoBox.html(value);
            infoBox.show();
        }
        else { infoBox.hide(); }
    }

    var polygonMode = false;
    var circleMode = false;
    var rectangleMode = false;
    this.toggleDrawMode = _toggleDrawMode;
    var myPoly = null;
    var phantomLine = null;
    var myRectangle = null;
	var myCircle = null;
	var radialLine = null;
    function _toggleDrawMode(mode) {
        polygonMode = (mode == 'poly');
        circleMode = (mode == 'circ');
        rectangleMode = (mode == 'rect');
        
        styleDeselected($('.drawMode'));
        styleSelected($('#drawMode_' + mode));
        
        $('#rectInfo1').hide();
        $('#rectInfo2').hide();
        $('#radiusInfo').hide();
        
        if (mapMouseUpListener != null) { google.maps.event.removeListener(mapMouseUpListener); mapMouseUpListener = null; }
        if (mapMouseMoveListener != null) { google.maps.event.removeListener(mapMouseMoveListener); mapMouseMoveListener = null; }
        
        destroyOverlay(myCircle); myCircle = null;
        destroyOverlay(myRectangle); myRectangle = null;
        destroyOverlay(myPoly); myPoly = null;
        destroyOverlay(radialLine); radialLine = null;
        destroyOverlay(phantomLine); phantomLine = null;
        
        polyMarkersSetMap();
        polyPointMarkers = [];
        midPointMarkers = [];

        if (circleMode || rectangleMode) { toggleDragging(false); } else { toggleDragging(true); }
    }
    
    function polyMarkersSetMap(m) {
        if (!m) { m = null; }
        for (i in polyPointMarkers) { polyPointMarkers[i].setMap(m); }
        for (i in midPointMarkers) { midPointMarkers[i].setMap(m); }
    }
    
    function finishDrawing() {
        if (circleMode) {
            //alert(sprintf("Circle Mode. Center = (%s,%s), Radius = %s", startLatLng.lat(), startLatLng.lng(), getRadiusText(startLatLng.distanceFrom(endLatLng)))); // TEMP
            setTimeout(function() { SearchBlockGroups("circle", sprintf("%s;%s", startLatLng.toUrlValue(), endLatLng.toUrlValue())); }, 200);
        } else if (rectangleMode) {
            //alert(sprintf("Rectangle Mode. Corner #1 = (%s,%s), Corner #2 = (%s,%s)", startLatLng.lat(), startLatLng.lng(), endLatLng.lat(), endLatLng.lng()));
            setTimeout(function() { SearchBlockGroups("rect", sprintf("%s;%s", startLatLng.toUrlValue(), endLatLng.toUrlValue())); }, 200);
        } else if (polygonMode) {
            if (myPoly != null) {
                //alert(sprintf("Polygon Mode. Number of points = %d", myPoly.getPath().getLength()));
                var points = [];
                myPoly.getPath().forEach(function(item) {
                    points.push(item.toUrlValue());
                });
                setTimeout(function() { SearchBlockGroups("poly", points.join(';')); }, 200);
            }
        }
        _toggleDrawMode('none');
    }
    
    function destroyOverlay(overlay) {
        if (overlay != null && overlay.setOptions) {
            overlay.setOptions({map:null});
        }
    }
    
    var fillColor = "#909090"; // gray fill
    var lineColor = "#000000"; // black line
    var phantomLineColor = "#ff0000";
    var opacity = .25;
    var lineWeight = 2;
    
    var polyPointMarkers = [];
    var midPointMarkers = [];
    var normalMarkerShadow = new google.maps.MarkerImage("http://www.google.com/mapfiles/shadow50.png", new google.maps.Size(37,34), new google.maps.Point(0,0), new google.maps.Point(9,34));
    var pointSearchTimeout = null;
    function addPolyPoint(e) {
//        if (this != null)
//        {
//            alert(typeof(this));
//        }
        if (polygonMode) {
            if (myPoly == null) {
                myPoly = new google.maps.Polygon({strokeColor:lineColor,strokeWeight:lineWeight,strokeOpacity:1,fillColor:fillColor,fillOpacity:opacity,map:map});
                google.maps.event.addListener(myPoly, "click", addPolyPoint);
            }
            myPoly.getPath().push(e.latLng);
        startLatLng = e.latLng;
            var marker = new google.maps.Marker({
              position: e.latLng,
              map: map,
              draggable: true
            });
            polyPointMarkers.push(marker);
            
            var numberOfPoints = myPoly.getPath().getLength();
            if (numberOfPoints > 1) {
                addMidPoint(getCenter(myPoly.getPath().getAt(numberOfPoints - 1), myPoly.getPath().getAt(numberOfPoints - 2)));
            }

            registerPolyPointEvents(marker);
        } else if (!circleMode && !rectangleMode) { // We're in select/deselect mode
            //alert("map click");
            if (pointSearchTimeout != null) { clearTimeout(pointSearchTimeout); pointSearchTimeout = null; }
            pointSearchTimeout = setTimeout(function() {
                SearchBlockGroups("point", e.latLng.toUrlValue(), 'selected=' + selectedBlockGroupIds);
            }, 200);
        }
    }
    
    function addMidPoint(midPoint) {
        midPointMarkers.push(getMidPointMarker(midPoint));
    }
    
    function getMidPointMarker(midPoint) {
        var marker = new google.maps.Marker({
            position: midPoint,
            map: map,
            draggable: true,
            icon: "http://www.google.com/mapfiles/zoom-plus-mini.png"
//            , shadow: normalMarkerShadow
        });
        marker.MouseDownListener = google.maps.event.addListener(marker, "mousedown", function() { convertMidPointToPolyPoint(this); });
        return marker;
    }
    
    function registerPolyPointEvents(marker) {
        google.maps.event.addListener(marker, 'click', clickPolyPointMarker);
        google.maps.event.addListener(marker, 'drag', dragPolyPointMarker);
        google.maps.event.addListener(marker, 'dragend', dragPolyPointMarker);
        google.maps.event.addListener(marker, 'mouseover', mouseoverPolyPointMarker);
        google.maps.event.addListener(marker, 'mouseout', mouseoutPolyPointMarker);
    }
    
    function convertMidPointToPolyPoint(marker) {
        var i = getMidPointMarkerIndex(marker);
        
        setMarkerIcon(marker, "marker");
        google.maps.event.removeListener(marker.MouseDownListener); marker.MouseDownListener = null;
        registerPolyPointEvents(marker);
        
        midPointMarkers.splice(i, 1,
            getMidPointMarker(getCenter(marker.getPosition(), polyPointMarkers[i].getPosition())),
            getMidPointMarker(getCenter(marker.getPosition(), polyPointMarkers[i + 1].getPosition()))
        );
        polyPointMarkers.splice(i + 1, 0, marker);
        myPoly.getPath().insertAt(i + 1, marker.getPosition());
    }
    
    function getMidPointMarkerIndex(marker) {
        for (var i = 0, I = midPointMarkers.length; i < I && midPointMarkers[i] != marker; ++i);
        return i;
    }
    
    function getPolyPointMarkerIndex(marker) {
        for (var i = 0, I = polyPointMarkers.length; i < I && polyPointMarkers[i] != marker; ++i);
        return i;
    }
    function mouseoverPolyPointMarker() {
        setMarkerIcon(this, "markerX");
    }
    function mouseoutPolyPointMarker() {
        setMarkerIcon(this, "marker");
    }
    function setMarkerIcon(marker, iconName) {
        marker.setIcon("http://www.google.com/mapfiles/" + iconName + ".png");
        marker.setShadow(normalMarkerShadow);
    }
    function clickPolyPointMarker() {
        this.setMap(null);
        var i = getPolyPointMarkerIndex(this);
        polyPointMarkers.splice(i, 1);
        if (i < midPointMarkers.length) {
            midPointMarkers[i].setMap(null);
            midPointMarkers.splice(i, 1);
        }
        if (i > 0) {
            midPointMarkers[i - 1].setMap(null);
            midPointMarkers.splice(i - 1, 1);
        }
        myPoly.getPath().removeAt(i);
        if (myPoly.getPath().getLength() > 0) {
            if (myPoly.getPath().getLength() == i) { // We just removed the most recently added point
                startLatLng = myPoly.getPath().getAt(i - 1);
                drawPhantomLine();
            } else if (i > 0) {
                midPointMarkers.splice(i - 1, 0, getMidPointMarker(getCenter(myPoly.getPath().getAt(i - 1), myPoly.getPath().getAt(i))));
            }
        } else { // We just removed the only point left
            destroyOverlay(myPoly); myPoly = null;
            destroyOverlay(phantomLine); phantomLine = null;
        }
    }
    function dragPolyPointMarker() {
        var i = getPolyPointMarkerIndex(this);
        if (myPoly.getPath().getLength() - 1 == i) { // We're moving the last point
            startLatLng = this.getPosition();
            drawPhantomLine();
        }
        if (i > 0) {
            midPointMarkers[i - 1].setPosition(getCenter(this.getPosition(), polyPointMarkers[i - 1].getPosition()));
        }
        if (i < myPoly.getPath().getLength() - 1) {
            midPointMarkers[i].setPosition(getCenter(this.getPosition(), polyPointMarkers[i + 1].getPosition()));
        }
        myPoly.getPath().setAt(i, this.getPosition());
    }
    
    function drawPhantomLine() {
        if (phantomLine != null) {
            phantomLine.setPath([startLatLng,endLatLng]);
        } else {
            phantomLine = new google.maps.Polyline({path:[startLatLng,endLatLng],strokeColor:phantomLineColor,strokeWeight:lineWeight,strokeOpacity:1,map:map,zIndex:200});
            google.maps.event.addListener(phantomLine, "click", addPolyPoint);
        }
    }

    function drawRectangle() {
        var p1 = new google.maps.LatLng(Math.min(startLatLng.lat(), endLatLng.lat()), Math.min(startLatLng.lng(), endLatLng.lng()));
		var p3 = new google.maps.LatLng(Math.max(startLatLng.lat(), endLatLng.lat()), Math.max(startLatLng.lng(), endLatLng.lng()));
		var p2 = new google.maps.LatLng(p1.lat(), p3.lng());
		var p4 = new google.maps.LatLng(p3.lat(), p1.lng());
        var bounds = new google.maps.LatLngBounds(p1, p3);

        if (myRectangle != null) { myRectangle.setOptions({bounds:bounds}); }
        else { myRectangle = new google.maps.Rectangle({bounds:bounds,strokeColor:lineColor,strokeWeight:lineWeight,strokeOpacity:1,fillColor:fillColor,fillOpacity:opacity,map:map}); }
        showRectSize(p1, p2, p4);
    }
    
    function showRectSize(p1, p2, p4) {
        _showLength(p1, p2, $('#rectInfo1'));
        _showLength(p1, p4, $('#rectInfo2'));
	}	
	
	this.showLength = _showLength;   
	
	function _showLength(p1, p2, infoBox, flip) {
	    var distance = p1.distanceFrom(p2);
	    
	    if (distance > 0) {
	        if (flip == null) { flip = false; }
	        infoBox.html(getRadiusText(distance));
	        infoBox.show();
    	    
		    var divPixel = projectionHelper.getProjection().fromLatLngToContainerPixel(getCenter(p1,p2));
		    var halfHeight = (infoBox.height() / 2);
		    var halfWidth = (infoBox.width() / 2);
		    var topModifier = (p1.lng() == p2.lng()) ? - halfHeight : (!flip ? 5 : -infoBox.height() - 5);
		    var leftModifier = (p1.lat() == p2.lat()) ? - halfWidth : (flip ? 5 : - infoBox.width() - 5);
		    infoBox.css("top", (mapCanvasOffset.top + divPixel.y + topModifier) + "px");
		    infoBox.css("left", (mapCanvasOffset.left + divPixel.x + leftModifier) + "px");
		} else {
		    infoBox.hide();
		}
	}
	
    function drawCircle() {

        var radius = startLatLng.distanceFrom(endLatLng);

        if (myCircle != null) {
            myCircle.setOptions({center:startLatLng,radius:radius});
        } else {
            myCircle = new google.maps.Circle({center:startLatLng,radius:radius,strokeColor:lineColor,strokeWeight:lineWeight,strokeOpacity:1,fillColor:fillColor,fillOpacity:opacity,map:map});
        }
		
		showRadius(radius); 
	}

	function showRadius(radiusInMeters) {		
		var projection = map.getProjection();
		var centerPt = projection.fromLatLngToPoint(startLatLng);
		var radiusPt = projection.fromLatLngToPoint(endLatLng);
		var radius = Math.sqrt(Math.pow((centerPt.x-radiusPt.x),2) + Math.pow((centerPt.y-radiusPt.y),2));
		
		var myX = centerPt.x + ((centerPt.x > radiusPt.x) ? radius: -radius);
		var myEndLatLng = projection.fromPointToLatLng(new google.maps.Point(myX, centerPt.y));
		
		if (radialLine != null) {
		    radialLine.setPath([startLatLng,myEndLatLng]);
        } else {
		    radialLine = new google.maps.Polyline({path:[startLatLng,myEndLatLng],map:map,strokeColor:lineColor,strokeWeight:lineWeight,strokeOpacity:1});
		}
		
		_showLength(startLatLng, myEndLatLng, $('#radiusInfo'), (startLatLng.lat() > endLatLng.lat()));
	}

	function getRadiusText(radiusInMeters) {
		var myText = '';
		if (radiusInMeters != null) {
			var radiusInMiles = cirrus.math.roundToXPlaces(radiusInMeters / 1609.44,2);
			myText = radiusInMiles + " mi";
		}
		return myText;
	}	
	
	function getCenter(p1, p2) {
	    var myP1 = new google.maps.LatLng(Math.min(p1.lat(), p2.lat()), Math.min(p1.lng(), p2.lng()));
		var myP2 = new google.maps.LatLng(Math.max(p1.lat(), p2.lat()), Math.max(p1.lng(), p2.lng()));
		return new google.maps.LatLngBounds(myP1, myP2).getCenter();
	}

    function toggleDragging(value) {
        map.setOptions({draggable: value});
    }
}

/* HACK: This replicates functionality from Google Maps V2 API that is missing from V3 */
google.maps.LatLng.prototype.distanceFrom = function(newLatLng) {
  //var R = 6371; // km (change this constant to get miles)
  var R = 6371000; // meters
  var lat1 = this.lat();
  var lon1 = this.lng();
  var lat2 = newLatLng.lat();
  var lon2 = newLatLng.lng();
  var dLat = (lat2-lat1) * Math.PI / 180;
  var dLon = (lon2-lon1) * Math.PI / 180;
  var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
    Math.cos(lat1 * Math.PI / 180 ) * Math.cos(lat2 * 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;
}

function str_repeat(i, m) {
	for (var o = []; m > 0; o[--m] = i);
	return o.join('');
}

function sprintf() {
	var i = 0, a, f = arguments[i++], o = [], m, p, c, x, s = '';
	while (f) {
		if (m = /^[^\x25]+/.exec(f)) {
			o.push(m[0]);
		}
		else if (m = /^\x25{2}/.exec(f)) {
			o.push('%');
		}
		else if (m = /^\x25(?:(\d+)\$)?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(f)) {
			if (((a = arguments[m[1] || i++]) == null) || (a == undefined)) {
				throw('Too few arguments.');
			}
			if (/[^s]/.test(m[7]) && (typeof(a) != 'number')) {
				throw('Expecting number but found ' + typeof(a));
			}
			switch (m[7]) {
				case 'b': a = a.toString(2); break;
				case 'c': a = String.fromCharCode(a); break;
				case 'd': a = parseInt(a); break;
				case 'e': a = m[6] ? a.toExponential(m[6]) : a.toExponential(); break;
				case 'f': a = m[6] ? parseFloat(a).toFixed(m[6]) : parseFloat(a); break;
				case 'o': a = a.toString(8); break;
				case 's': a = ((a = String(a)) && m[6] ? a.substring(0, m[6]) : a); break;
				case 'u': a = Math.abs(a); break;
				case 'x': a = a.toString(16); break;
				case 'X': a = a.toString(16).toUpperCase(); break;
			}
			a = (/[def]/.test(m[7]) && m[2] && a >= 0 ? '+'+ a : a);
			c = m[3] ? m[3] == '0' ? '0' : m[3].charAt(1) : ' ';
			x = m[5] - String(a).length - s.length;
			p = m[5] ? str_repeat(c, x) : '';
			o.push(s + (m[4] ? a + p : p + a));
		}
		else {
			throw('Huh ?!');
		}
		f = f.substring(m[0].length);
	}
	return o.join('');
}
