﻿/*
GoogleMap.js           
This is used by the GoogleMap server control to display Google Maps.

Initial Version:                Colin Bakewell      17 May 2008
Current extended Version:       Anton Gerassimov    27 Sep 2008
Fixed for IE6                   Colin Bakewell      17 Dec 2008
*/


/*
This waits for the google maps api to load
*/
var jpgminitcallback = null;

function delayed_callbackcall() {
        window.defaultStatus = "Initialising Maps" ; 
        if(jpgminitcallback != null) { jpgminitcallback() ; }
}

function waitforgooglemaps() { 
    if ((GMap2 == undefined) || (GMarker == undefined)) {
        window.setTimeout("waitforgooglemaps();",500) ;
        window.defaultStatus = "Still waiting for GoogleMaps" ;
    }
    else {
        window.setTimeout("delayed_callbackcall();",500) ;   
        window.defaultStatus = "Waiting to initialise Maps" ;         
    }
}

function wait_init_googlemaps(callback) {
    jpgminitcallback = callback ;
    window.setTimeout("waitforgooglemaps();",500) ;
    window.defaultStatus = "Started waiting for GoogleMaps" ;
}

/*
Google Map related code
*/
function GoogleMap() {
    this.mycount = 0;
    this.map = null;
    this.markermap = new Array();
    this.centreLatitude = 51.48;
    this.centreLongitude = 0;
    this.startMag = 13;
    this.mapDiv = "map_canvas";
    this.addrInput = "addrin";
    this.searchTextInputID = "searchtext1";
    this.searchButtonID = "searchbutton1";
    this.searchDropDownID = "selectfrom1";
    this.updateLocationUrl = "";
    this.renameInput = "renameval";
    this.locationIds = new Array;
    this.markertemplateName = "";
    this.allowMarkerMoving = true
    this.allowMarkerCreation = true
    this.allowMapOverlays = true
    this.minimumLatitude = 52.1      // used to find lat long bounds
    this.maximumLatitude = 52.2      // used to find lat long bounds
    this.mininumLongitude = 0.1      // used to find lat long bounds
    this.maximumLongitude = 0.2      // used to find lat long bounds
    this.showInfoOnMouseOver = true;
    this.markerHasBeenClicked = false;
    this.showInfo = false;
    this.doubleClickDestination = "";
    this.enableScrollWheelZoom = true;
    this.useSmallMapControl = false;
    this.displayScaleControl = false;
    this.displayOverviewMapControl = false;
    this.displayMapTypeControl = false;
    this.geocoder = new GClientGeocoder();
    this.circles = [];
}

/*Functions for marker dragging and updating*/
GoogleMap.prototype.markerDragStart = function() {
    this.closeInfoWindow();
}

GoogleMap.prototype.markerDragEnd = function(e) {
    this.parentObject.updateLocation(this);
}

GoogleMap.prototype.updateLocation = function(e) {

    var urlPath;
    var point = e.getLatLng();
    var url = "";
    var businessId;
    var businessQueryString = "";


    url = document.location.toString();
    businessId = url.indexOf("?");
    businessQueryString = url.substring(businessId + 1);
    urlPath = this.updateLocationUrl + "?Longitude=" +  point.x + "&Latitude=" + point.y + "&" + businessQueryString;
    this.getDateAjax(urlPath);

}

GoogleMap.prototype.getDateAjax = function(path) {
    if (window.XMLHttpRequest) {
        xhttpAjax = new XMLHttpRequest();
    }
    else if (window.ActiveXObject) {
        xhttpAjax = new ActiveXObject("Msxml2.XMLHTTP");
    }
    xhttpAjax.onreadystatechange = this.onReadyStateAjax;
    xhttpAjax.open("GET", path, true);
    xhttpAjax.send("");
}


GoogleMap.prototype.onReadyStateAjax = function() {

    var deleteItem;
    var calendarDiv;

    if (xhttpAjax.readyState == 4) {
        var commandData = xhttpAjax.responseText;
        if (commandData != "True") {
            alert("Longitude and Latitude failed to be updated: " + commandData);
        }
    }
}
/**/


GoogleMap.prototype.formatAddress = function(address) {
    var formattedAddress = "";
    var i = 0;
    var addressList = address.split(",");

    var numberOfElements = addressList.length;
    if (numberOfElements > 0) { formattedAddress += "<b><big>"; }
    for (i = 0; i < numberOfElements; i++) {
        formattedAddress += addressList[i];
        if (i == 0) {
            formattedAddress += "</big><small>";
        }
        formattedAddress += "<br />";
    }
    formattedAddress += "</small></b>";
    return formattedAddress;
}

GoogleMap.prototype.showMarkerInfo = function(marker) {

    var infoWindowHTML = "";
    if (document.getElementById(marker.templateName) != null) {
        infoWindowHTML = document.getElementById(marker.templateName).innerHTML
    }
    else {
        infoWindowHTML = marker.templateName
    }
    marker.openInfoWindowHtml(infoWindowHTML);

}

GoogleMap.prototype.markerClicked = function() {
    this.parentObject.markerHasBeenClicked = true;
    if (!this.parentObject.showInfoOnMouseOver) { this.parentObject.showMarkerInfo(this); };
}
// marker double click handler - goes to a url
GoogleMap.prototype.markerDoubleClicked = function() {
    window.location = this.doubleClickDestination;
}

GoogleMap.prototype.markerMouseOver = function() {
    this.parentObject.markerHasBeenClicked = false;
    this.parentObject.showMarkerInfo(this);
}
GoogleMap.prototype.markerMouseOut = function() {
    if (!this.parentObject.markerHasBeenClicked) { this.closeInfoWindow(); }
}

GoogleMap.prototype.removePreviousLayers = function() {
    //This function will delete a previous layer if the previous layer isn't the first one
    if (this.mycount > 1) {
        var firstMarker = this.markermap[1];
        this.map.clearOverlays();
        this.map.addOverlay(firstMarker);
    }
}

GoogleMap.prototype.createNewMarkerWithIcon = function(LatLngLatitude, LatLngLongitude, mytitle, templateName, myicon) {
    var point;
    var marker;

    // check if this marker is exactly on top of another one. If so adjust the Latitude Longitude a bit to make sure it's not on top
    point = new GLatLng(LatLngLatitude, LatLngLongitude);
    positionOkay = 0;

    while (positionOkay == 0) {
        positionOkay = 1;
        for (i = 1; i <= this.mycount; i++) {

            if (point.distanceFrom(this.markermap[i].getLatLng()) < 5.0) {
                var latitudeOffset;
                var longitudeOffset;
                var minimumOffset = 0.3;
                var offsetdivisor = 3000.0;
                latitudeOffset = (Math.random() - 0.5);
                if (latitudeOffset >= 0) latitudeOffset += minimumOffset; else latitudeOffset -= minimumOffset;
                longitudeOffset = (Math.random() - 0.5);
                if (longitudeOffset >= 0) longitudeOffset += minimumOffset; else longitudeOffset -= minimumOffset;
                LatLngLatitude += latitudeOffset / offsetdivisor;
                LatLngLongitude += longitudeOffset / offsetdivisor;
                point = new GLatLng(LatLngLatitude, LatLngLongitude);
                positionOkay = 0;      // force it to be rechecked     
            }
        }
    }

    if (myicon == null) {
        myicon = new GIcon(G_DEFAULT_ICON);
    }
    if (this.allowMarkerMoving) {
        marker = new GMarker(point, { draggable: true, title: mytitle, icon: myicon });
    }
    else {
        marker = new GMarker(point, { draggable: false, title: mytitle, icon: myicon });
    }

    this.mycount = this.mycount + 1;
    this.markermap[this.mycount] = marker
    marker.value = this.mycount;
    marker.templateName = templateName;
    marker.parentObject = this;
    if (this.showInfoOnMouseOver) {
        GEvent.addListener(marker, "mouseover", this.markerMouseOver);
        GEvent.addListener(marker, "mouseout", this.markerMouseOut);
        GEvent.addListener(marker, "click", this.markerClicked);
    }
    else {
        GEvent.addListener(marker, "click", this.markerClicked);
    }
    if (this.allowMarkerMoving) {
        GEvent.addListener(marker, "dragstart", this.markerDragStart);
        GEvent.addListener(marker, "dragend", this.markerDragEnd);
    }

    this.lastOverlay = marker;
    this.map.addOverlay(marker);
    if (this.minimumLatitude > LatLngLatitude) this.minimumLatitude = LatLngLatitude;
    if (this.maximumLatitude < LatLngLatitude) this.maximumLatitude = LatLngLatitude;
    if (this.mininumLongitude > LatLngLongitude) this.mininumLongitude = LatLngLongitude;
    if (this.maximumLongitude < LatLngLongitude) this.maximumLongitude = LatLngLongitude;
    if (this.showInfo) {
        this.showMarkerInfo(marker);
        this.showInfo = false;
    }
}

GoogleMap.prototype.createNewMarker = function(LatLngLatitude, LatLngLongitude, mytitle, templateName) {
    this.createNewMarkerWithIcon(LatLngLatitude, LatLngLongitude, mytitle, templateName, null);
}

// create in response to direct call - this points to GoogleMap object
GoogleMap.prototype.createTemplatedMarker = function(LatLngLatitude, LatLngLongitude, mytitle, templateName) {
    this.createNewMarker(LatLngLatitude, LatLngLongitude, mytitle, templateName);
}

// create in response to direct call - this points to GoogleMap object
GoogleMap.prototype.createTemplatedMarkerWithIcon = function(LatLngLatitude, LatLngLongitude, mytitle, myIconImage, myIconShadow, templateName, iconWidth, iconHeight) {


    // these image referances need to be accessable urls ie http://........

    var customIcon = new GIcon(G_DEFAULT_ICON);
    if (iconWidth > "" && iconHeight > "") { customIcon.iconSize = new GSize(iconWidth, iconHeight); }
    if (myIconImage > "") { customIcon.image = myIconImage; }
    if (myIconShadow > "") { customIcon.shadow = myIconShadow; }

    this.createNewMarkerWithIcon(LatLngLatitude, LatLngLongitude, mytitle, templateName, customIcon);
}


// create in response to click on map - this points to GoogleMap object
GoogleMap.prototype.createMarker = function(LatLngLatitude, LatLngLongitude, mytitle) {
    this.createNewMarker(LatLngLatitude, LatLngLongitude, mytitle, "");
}

GoogleMap.prototype.mapClicked = function(overlay, LatLng) {
    if ((LatLng != null) && (overlay == null)) {
        this.parentObject.createMarker(LatLng.y, LatLng.x, "New Location");
    }
}

GoogleMap.prototype.centreAt = function(LatitudeLongitudeLatitude, LatitudeLongitudeLongitude, Magnification) {
    this.centreLatitude = LatitudeLongitudeLatitude;
    this.centreLongitude = LatitudeLongitudeLongitude;
    this.startMag = Magnification;
    this.map.setCenter(new GLatLng(this.centreLatitude, this.centreLongitude), this.startMag);
}

GoogleMap.prototype.centreAtAutoZoom = function(LatitudeLongitudeLatitude, LatitudeLongitudeLongitude) {
    this.centreLatitude = LatitudeLongitudeLatitude;
    this.centreLongitude = LatitudeLongitudeLongitude;

    reqZoom = this.map.getBoundsZoomLevel(new GLatLngBounds(new GLatLng(this.minimumLatitude, this.mininumLongitude), new GLatLng(this.maximumLatitude, this.maximumLongitude)));
    reqZoom = Math.min(16, reqZoom);
    this.map.setCenter(new GLatLng(this.centreLatitude, this.centreLongitude), reqZoom);
}

GoogleMap.prototype.getCenter = function() {
    return (this.map.getCenter());
}

GoogleMap.prototype.map = function() {
    return (this.map);
}

GoogleMap.prototype.initialize = function() {

    // Wait for google api to load .. needed for ie6
    var date = new Date;
    var timestarted = date.getTime();
    var now = date.getTime();
    while ((GMap2 == undefined) && ((now - timestarted) < 10000)) {
        now = date.getTime();

    }
    
    //Initialise search objects
    this.searchTextInputObj = document.getElementById(this.searchTextInputID);
    this.searchButtonObj = document.getElementById(this.searchButtonID);
    this.searchDropDownObj = document.getElementById(this.searchDropDownID);
    if (this.searchButtonObj != null) {
        this.searchButtonObj.GoogleMapObject = this;
        this.searchButtonObj.onclick = this.showAddress;
    }
    if (this.searchDropDownObj != null) {
        this.searchDropDownObj.style.display = "none";
    }
    

    this.mycount = 0;

    if (GBrowserIsCompatible()) {
        this.map = new GMap2(document.getElementById(this.mapDiv));
        this.map.setCenter(new GLatLng(this.centreLatitude, this.centreLongitude), this.startMag);

        if (this.useSmallMapControl)
            this.map.addControl(new GSmallMapControl());
        else
            this.map.addControl(new GLargeMapControl());

        if (this.displayMapTypeControl) { this.map.addControl(new GMapTypeControl()); }
        if (this.displayScaleControl) { this.map.addControl(new GScaleControl()); }

        // Display the overview control in a closed state
        if (this.displayOverviewMapControl) {
            var ovc = new GOverviewMapControl();
            this.map.addControl(ovc);
            ovc.hide(true);
        }

        this.map.parentObject = this;
        if (this.allowMarkerCreation) {
            GEvent.addListener(this.map, "click", this.mapClicked)
        }

        if (this.enableScrollWheelZoom) this.map.enableScrollWheelZoom();

        this.minimumLatitude = 99999;
        this.maximumLatitude = -99999;
        this.mininumLongitude = 99999;
        this.maximumLongitude = -99999;
    }
}

GoogleMap.prototype.mapClicked = function(overlay, latlng) {
    if ((latlng != null) && (overlay == null)) {
        this.parentObject.createMarker(latlng.y, latlng.x, "New Location");
    }
}

GoogleMap.prototype.gotoplace = function() {
    this.GoogleMapObject.searchTextInputObj.value = this.GoogleMapObject.searchDropDownObj.options[this.GoogleMapObject.searchDropDownObj.selectedIndex].text;
    while (this.GoogleMapObject.searchDropDownObj.length > 0) {
        this.GoogleMapObject.searchDropDownObj.options.remove(0);
    }
    this.GoogleMapObject.showAddress();

}

GoogleMap.prototype.processGeocode = function(response) {

    var mygmo = this.GoogleMapObject

    if (!response || response.Status.code != 200) {

        switch (response.Status.code) {
            case 200:
                alert("G_GEO_SUCCESS (200) No errors occurred; the address was successfully parsed and its geocode has been returned. (Since 2.55) ");
                break;
            case 400:
                alert("G_GEO_BAD_REQUEST (400) A directions request could not be successfully parsed. (Since 2.81) ");
                break;
            case 500:
                alert("G_GEO_SERVER_ERROR (500) A geocoding or directions request could not be successfully processed, yet the exact reason for the failure is not known. (Since 2.55) ");
                break;
            case 601:
                alert("G_GEO_MISSING_QUERY (601) The HTTP q parameter was either missing or had no value. For geocoding requests, this means that an empty address was specified as input. For directions requests, this means that no query was specified in the input. (Since 2.81) ");
                break;
            case 602:
                alert("G_GEO_UNKNOWN_ADDRESS (602) No corresponding geographic location could be found for the specified address. This may be due to the fact that the address is reLatitudeively new, or it may be incorrect. (Since 2.55) ");
                break;
            case 603:
                alert("G_GEO_UNAVAILABLE_ADDRESS (603) The geocode for the given address or the route for the given directions query cannot be returned due to legal or contractual reasons. (Since 2.55) ");
                break;
            case 604:
                alert("G_GEO_UNKNOWN_DIRECTIONS (604) The GDirections object could not compute directions between the points mentioned in the query. This is usually because there is no route available between the two points, or because we do not have data for routing in that region. (Since 2.81) ");
                break;
            case 610:
                alert("G_GEO_BAD_KEY (610) The given key is either invalid or does not match the domain for which it was given. (Since 2.55) ");
                break;
            case 620:
                alert("G_GEO_TOO_MANY_QUERIES (620) The given key has gone over the requests limit in the 24 hour period. (Since 2.55) ");
                break;
            default:
                alert("Unknown Error: (" + respone.Status.code + ")");
                break;
        }

    } else {

        if (response.Placemark.length > 1) {
            opt = new Option("please select from...");
            mygmo.searchDropDownObj.options.add(opt);
            for (var i = 0; i < response.Placemark.length; i++) {
                place = response.Placemark[i];
                opt = new Option(place.address);
                mygmo.searchDropDownObj.options.add(opt);
            }

            mygmo.searchDropDownObj.GoogleMapObject = mygmo
            mygmo.searchDropDownObj.onchange = mygmo.gotoplace;
            mygmo.searchDropDownObj.style.display = "inline";
        }
        else {
            mygmo.searchDropDownObj.innerHTML = "";
            mygmo.searchDropDownObj.style.display = "none";
            place = response.Placemark[0];
            point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);

            mygmo.map.setCenter(point, 13);

            if (mygmo.mycount == 1) {
                mygmo.markermap[1].deleteme();
                mygmo.mycount = 0;

            }

            mygmo.createMarker(point.y, point.x, place.address);
            mygmo.updateLocation(mygmo.markermap[mygmo.mycount]);
        }
    }
}

GoogleMap.prototype.showAddress = function() {
    var address = "";
    if (this.GoogleMapObject != null) {
        address = this.GoogleMapObject.searchTextInputObj.value;
        window.GoogleMapObject = this.GoogleMapObject;
        this.GoogleMapObject.geocoder.getLocations(address, this.GoogleMapObject.processGeocode);
    }
    else {
        address = this.searchTextInputObj.value;
        window.GoogleMapObject = this;
        this.geocoder.getLocations(address, this.processGeocode);
    }
    return (false);
}

// directly specify an address to go to - must be unique address or else the select address drop down will show !
GoogleMap.prototype.showAddressDirect = function(addr) {
    var address = "";
    if (this.GoogleMapObject != null) {
        address = addr;
        window.GoogleMapObject = this.GoogleMapObject;
        this.GoogleMapObject.geocoder.getLocations(address, this.GoogleMapObject.processGeocode);
    }
    else {
        address = addr;
        window.GoogleMapObject = this;
        this.geocoder.getLocations(address, this.processGeocode);
    }
    return (false);
}

// add an on click event to a highlighter to show the reLatitudeed marker
GoogleMap.prototype.createMarkerHighlighterClick = function(markerNo, HighligherID) {

    var theHighlighter = document.getElementById(HighligherID);
    if ((theHighlighter != null) && (markerNo > 0)) {
        theHighlighter.myClickMarker = this.markermap[markerNo];
        theHighlighter.onclick = this.markermap[markerNo].showMyInfoClick;
    }
}
// add an on mouse over and out events to a highlighter to show the reLatitudeed marker
GoogleMap.prototype.createMarkerHighlighterOver = function(markerNo, HighligherID) {

    var theHighlighter = document.getElementById(HighligherID);
    if ((theHighlighter != null) && (markerNo > 0)) {
        theHighlighter.myOverMarker = this.markermap[markerNo];
        theHighlighter.onmouseover = this.markermap[markerNo].showMyInfoOver;
        theHighlighter.onmouseout = this.markermap[markerNo].closeInfoWindow;
    }

}
// add a Destination url to go to when marker is double clicked 
GoogleMap.prototype.addDoubleClickDestination = function(markerNo, destination) {
    this.markermap[markerNo].doubleClickDestination = destination;
    GEvent.addListener(this.markermap[markerNo], "dblclick", this.markerDoubleClicked);
}

/*
Google Marker related code
*/
GMarker.prototype.deleteme = function() {
    this.closeInfoWindow();
    this.parentObject.map.removeOverlay(this);
}

GMarker.prototype.deletemarker = function(e) {
    this.markerObject.deleteme();
}

GMarker.prototype.dorename = function(e) {
    var point = this.markerObject.getLatLng();
    var theRenameInput = document.getElementById(this.markerObject.parentObject.renameInput);
    this.markerObject.parentObject.showInfo = true;
    this.markerObject.parentObject.createMarker(point.y, point.x, theRenameInput.value);
    this.markerObject.deleteme();
}

GMarker.prototype.showMyInfoClick = function(e) {
    this.myClickMarker.parentObject.showMarkerInfo(this.myClickMarker);
}
GMarker.prototype.showMyInfoOver = function(e) {
    this.myOverMarker.parentObject.showMarkerInfo(this.myOverMarker);
}

// event handler for highlight click
GMarker.prototype.showMyInfoClick = function(e) {
    this.myClickMarker.parentObject.showMarkerInfo(this.myClickMarker);
}

// event handler for highlight over
GMarker.prototype.showMyInfoOver = function(e) {
    this.myOverMarker.parentObject.showMarkerInfo(this.myOverMarker);
}


//  Function to draw a polygon overlay on to the map.
//  This function will display, connect and fill a polygon with the specified
//  number of sides. 3 sides - triangle, 4 sides - square, 360 displays a circle etc.
//
//  centerLat               // Latitude of center point
//  centerLng               // Longitude of center point
//  radius                  // radius in miles of polygon vertices (1km = 0.62m)
//  sides                   // nos. of sides of polygon (use 36 for circle)
//  color                   // color of polygon border (HEX)
//  width                   // width of polygon border (pixels)
//  opacity                 // opacity of polygon border (0-1)
//  backgroundColor         // circle background fill color (HEX)
//  backgroundOpacity       // circle background fill opacity (0-1)

GoogleMap.prototype.polygon = function(centerLat, centerLng, radius, sides, color, width, opacity, backgroundColor, backgroundOpacity) {

    var bounds = this.map.getBounds();
    var southWest = bounds.getSouthWest();
    var northEast = bounds.getNorthEast();
    //var center = GLatLng(centerLat, centerLng);

    var PGpoints = [];
    var PGlat = (radius / 3963) * 180 / Math.PI;
    // using 3963 miles as earth's radius in miles
    var PGlng = PGlat / Math.cos(centerLat * Math.PI / 180);

    for (var i = -1; i < sides; i++) {
        var theta = ((2 * i + 1) / sides - 0.5) * Math.PI;
        var PGx = centerLng + (PGlng * Math.cos(theta));
        var PGy = centerLat + (PGlat * Math.sin(theta));
        PGpoints.push(new GLatLng(PGy, PGx));
    };
    this.map.addOverlay(new GPolygon(PGpoints, color, width, opacity, backgroundColor, backgroundOpacity));
}

function OSRefToMap() {
    return "x=" + this.easting + "&y=" + this.northing;
}

// The remainder of this file is jscoord.js, written by Jonathan Stott
// Licensed under the GNU General Public License (GPL)  
//
// http://www.jstott.me.uk/jscoord/

//--------------------------------------------------------------------------
// JScoord
// jscoord.js
//
// (c) 2005 Jonathan Stott
//
// Created on 21-Dec-2005
//
// 1.1.1 - 16 Jan 2006
//  - Fixed radix bug in getOSRefFromSixFigureReference that would return
//    the incorrect reference if either the northing or the easting started
//    with a leading zero.
// 1.1 - 23 Dec 2005
//  - Added getOSRefFromSixFigureReference function.
// 1.0 - 11 Aug 2005
//  - Initial version created from PHPcoord v1.1
//--------------------------------------------------------------------------


// ================================================================== LatLng

function LatLng(lat, lng) {
    this.lat = lat;
    this.lng = lng;

    this.distance = LatLngDistance;

    this.toOSRef = LatLngToOSRef;
    this.toUTMRef = LatLngToUTMRef;

    this.WGS84ToOSGB36 = WGS84ToOSGB36;
    this.OSGB36ToWGS84 = OSGB36ToWGS84;

    this.toString = LatLngToString;
}

function LatLngToString() {
    return "(" + this.lat + ", " + this.lng + ")";
}



// =================================================================== OSRef

// References given with OSRef are accurate to 1m.
function OSRef(easting, northing) {
    this.easting = easting;
    this.northing = northing;

    this.toLatLng = OSRefToLatLng;

    this.toString = OSRefToString;
    this.toSixFigureString = OSRefToSixFigureString;
    this.toMap = OSRefToMap;
}


function OSRefToString() {
    return "(" + this.easting + ", " + this.northing + ")";
}


function OSRefToSixFigureString() {
    var hundredkmE = Math.floor(this.easting / 100000);
    var hundredkmN = Math.floor(this.northing / 100000);
    var firstLetter = "";
    if (hundredkmN < 5) {
        if (hundredkmE < 5) {
            firstLetter = "S";
        } else {
            firstLetter = "T";
        }
    } else if (hundredkmN < 10) {
        if (hundredkmE < 5) {
            firstLetter = "N";
        } else {
            firstLetter = "O";
        }
    } else {
        firstLetter = "H";
    }

    var secondLetter = "";
    var index = 65 + ((4 - (hundredkmN % 5)) * 5) + (hundredkmE % 5);
    var ti = index;
    if (index >= 73) index++;
    secondLetter = chr(index);

    var e = Math.floor((this.easting - (100000 * hundredkmE)) / 100);
    var n = Math.floor((this.northing - (100000 * hundredkmN)) / 100);
    var es = e;
    if (e < 100) es = "0" + es;
    if (e < 10) es = "0" + es;
    var ns = n;
    if (n < 100) ns = "0" + ns;
    if (n < 10) ns = "0" + ns;

    return firstLetter + secondLetter + es + ns;
}


// ================================================================== UTMRef

function UTMRef(easting, northing, latZone, lngZone) {
    this.easting = easting;
    this.northing = northing;
    this.latZone = latZone;
    this.lngZone = lngZone;

    this.toLatLng = UTMRefToLatLng;

    this.toString = UTMRefToString;
}


function UTMRefToString() {
    return this.lngZone + this.latZone + " " +
         this.easting + " " + this.northing;
}


// ================================================================== RefEll

function RefEll(maj, min) {
    this.maj = maj;
    this.min = min;
    this.ecc = ((maj * maj) - (min * min)) / (maj * maj);
}


// ================================================== Mathematical Functions

function sinSquared(x) {
    return Math.sin(x) * Math.sin(x);
}

function cosSquared(x) {
    return Math.cos(x) * Math.cos(x);
}

function tanSquared(x) {
    return Math.tan(x) * Math.tan(x);
}

function sec(x) {
    return 1.0 / Math.cos(x);
}

function deg2rad(x) {
    return x * (Math.PI / 180);
}

function rad2deg(x) {
    return x * (180 / Math.PI);
}

function chr(x) {
    var h = x.toString(16);
    if (h.length == 1)
        h = "0" + h;
    h = "%" + h;
    return unescape(h);
}

function ord(x) {
    var c = x.charAt(0);
    var i;
    for (i = 0; i < 256; ++i) {
        var h = i.toString(16);
        if (h.length == 1)
            h = "0" + h;
        h = "%" + h;
        h = unescape(h);
        if (h == c)
            break;
    }
    return i;
}


// ========================================================= Other Functions

function LatLngDistance(to) {
    var er = 6366.707;

    var latFrom = deg2rad(this.lat);
    var latTo = deg2rad(to.lat);
    var lngFrom = deg2rad(this.lng);
    var lngTo = deg2rad(to.lng);

    var x1 = er * Math.cos(lngFrom) * Math.sin(latFrom);
    var y1 = er * Math.sin(lngFrom) * Math.sin(latFrom);
    var z1 = er * Math.cos(latFrom);

    var x2 = er * Math.cos(lngTo) * Math.sin(latTo);
    var y2 = er * Math.sin(lngTo) * Math.sin(latTo);
    var z2 = er * Math.cos(latTo);

    var d = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2));

    return d;
}


// ==================================================== Conversion Functions

function OSGB36ToWGS84() {
    var airy1830 = new RefEll(6377563.396, 6356256.909);
    var a = airy1830.maj;
    var b = airy1830.min;
    var eSquared = airy1830.ecc;
    var phi = deg2rad(this.lat);
    var lambda = deg2rad(this.lng);
    var v = a / (Math.sqrt(1 - eSquared * sinSquared(phi)));
    var H = 0; // height
    var x = (v + H) * Math.cos(phi) * Math.cos(lambda);
    var y = (v + H) * Math.cos(phi) * Math.sin(lambda);
    var z = ((1 - eSquared) * v + H) * Math.sin(phi);

    var tx = 446.448;
    var ty = -124.157;
    var tz = 542.060;
    var s = -0.0000204894;
    var rx = deg2rad(0.00004172222);
    var ry = deg2rad(0.00006861111);
    var rz = deg2rad(0.00023391666);

    var xB = tx + (x * (1 + s)) + (-rx * y) + (ry * z);
    var yB = ty + (rz * x) + (y * (1 + s)) + (-rx * z);
    var zB = tz + (-ry * x) + (rx * y) + (z * (1 + s));

    var wgs84 = new RefEll(6378137.000, 6356752.3141);
    a = wgs84.maj;
    b = wgs84.min;
    eSquared = wgs84.ecc;

    var lambdaB = rad2deg(Math.atan(yB / xB));
    var p = Math.sqrt((xB * xB) + (yB * yB));
    var phiN = Math.atan(zB / (p * (1 - eSquared)));
    for (var i = 1; i < 10; i++) {
        v = a / (Math.sqrt(1 - eSquared * sinSquared(phiN)));
        phiN1 = Math.atan((zB + (eSquared * v * Math.sin(phiN))) / p);
        phiN = phiN1;
    }

    var phiB = rad2deg(phiN);

    this.lat = phiB;
    this.lng = lambdaB;
}

function WGS84ToOSGB36() {
    var wgs84 = new RefEll(6378137.000, 6356752.3141);
    var a = wgs84.maj;
    var b = wgs84.min;
    var eSquared = wgs84.ecc;
    var phi = deg2rad(this.lat);
    var lambda = deg2rad(this.lng);
    var v = a / (Math.sqrt(1 - eSquared * sinSquared(phi)));
    var H = 0; // height
    var x = (v + H) * Math.cos(phi) * Math.cos(lambda);
    var y = (v + H) * Math.cos(phi) * Math.sin(lambda);
    var z = ((1 - eSquared) * v + H) * Math.sin(phi);

    var tx = -446.448;
    var ty = 124.157;
    var tz = -542.060;
    var s = 0.0000204894;
    var rx = deg2rad(-0.00004172222);
    var ry = deg2rad(-0.00006861111);
    var rz = deg2rad(-0.00023391666);

    var xB = tx + (x * (1 + s)) + (-rx * y) + (ry * z);
    var yB = ty + (rz * x) + (y * (1 + s)) + (-rx * z);
    var zB = tz + (-ry * x) + (rx * y) + (z * (1 + s));

    var airy1830 = new RefEll(6377563.396, 6356256.909);
    a = airy1830.maj;
    b = airy1830.min;
    eSquared = airy1830.ecc;

    var lambdaB = rad2deg(Math.atan(yB / xB));
    var p = Math.sqrt((xB * xB) + (yB * yB));
    var phiN = Math.atan(zB / (p * (1 - eSquared)));
    for (var i = 1; i < 10; i++) {
        v = a / (Math.sqrt(1 - eSquared * sinSquared(phiN)));
        phiN1 = Math.atan((zB + (eSquared * v * Math.sin(phiN))) / p);
        phiN = phiN1;
    }

    var phiB = rad2deg(phiN);

    this.lat = phiB;
    this.lng = lambdaB;
}

function OSRefToLatLng() {
    var airy1830 = new RefEll(6377563.396, 6356256.909);
    var OSGB_F0 = 0.9996012717;
    var N0 = -100000.0;
    var E0 = 400000.0;
    var phi0 = deg2rad(49.0);
    var lambda0 = deg2rad(-2.0);
    var a = airy1830.maj;
    var b = airy1830.min;
    var eSquared = airy1830.ecc;
    var phi = 0.0;
    var lambda = 0.0;
    var E = this.easting;
    var N = this.northing;
    var n = (a - b) / (a + b);
    var M = 0.0;
    var phiPrime = ((N - N0) / (a * OSGB_F0)) + phi0;
    do {
        M =
      (b * OSGB_F0)
        * (((1 + n + ((5.0 / 4.0) * n * n) + ((5.0 / 4.0) * n * n * n))
          * (phiPrime - phi0))
          - (((3 * n) + (3 * n * n) + ((21.0 / 8.0) * n * n * n))
            * Math.sin(phiPrime - phi0)
            * Math.cos(phiPrime + phi0))
          + ((((15.0 / 8.0) * n * n) + ((15.0 / 8.0) * n * n * n))
            * Math.sin(2.0 * (phiPrime - phi0))
            * Math.cos(2.0 * (phiPrime + phi0)))
          - (((35.0 / 24.0) * n * n * n)
            * Math.sin(3.0 * (phiPrime - phi0))
            * Math.cos(3.0 * (phiPrime + phi0))));
        phiPrime += (N - N0 - M) / (a * OSGB_F0);
    } while ((N - N0 - M) >= 0.001);
    var v = a * OSGB_F0 * Math.pow(1.0 - eSquared * sinSquared(phiPrime), -0.5);
    var rho =
    a
      * OSGB_F0
      * (1.0 - eSquared)
      * Math.pow(1.0 - eSquared * sinSquared(phiPrime), -1.5);
    var etaSquared = (v / rho) - 1.0;
    var VII = Math.tan(phiPrime) / (2 * rho * v);
    var VIII =
    (Math.tan(phiPrime) / (24.0 * rho * Math.pow(v, 3.0)))
      * (5.0
        + (3.0 * tanSquared(phiPrime))
        + etaSquared
        - (9.0 * tanSquared(phiPrime) * etaSquared));
    var IX =
    (Math.tan(phiPrime) / (720.0 * rho * Math.pow(v, 5.0)))
      * (61.0
        + (90.0 * tanSquared(phiPrime))
        + (45.0 * tanSquared(phiPrime) * tanSquared(phiPrime)));
    var X = sec(phiPrime) / v;
    var XI =
    (sec(phiPrime) / (6.0 * v * v * v))
      * ((v / rho) + (2 * tanSquared(phiPrime)));
    var XII =
    (sec(phiPrime) / (120.0 * Math.pow(v, 5.0)))
      * (5.0
        + (28.0 * tanSquared(phiPrime))
        + (24.0 * tanSquared(phiPrime) * tanSquared(phiPrime)));
    var XIIA =
    (sec(phiPrime) / (5040.0 * Math.pow(v, 7.0)))
      * (61.0
        + (662.0 * tanSquared(phiPrime))
        + (1320.0 * tanSquared(phiPrime) * tanSquared(phiPrime))
        + (720.0
          * tanSquared(phiPrime)
          * tanSquared(phiPrime)
          * tanSquared(phiPrime)));
    phi =
    phiPrime
      - (VII * Math.pow(E - E0, 2.0))
      + (VIII * Math.pow(E - E0, 4.0))
      - (IX * Math.pow(E - E0, 6.0));
    lambda =
    lambda0
      + (X * (E - E0))
      - (XI * Math.pow(E - E0, 3.0))
      + (XII * Math.pow(E - E0, 5.0))
      - (XIIA * Math.pow(E - E0, 7.0));

    return new LatLng(rad2deg(phi), rad2deg(lambda));
}


/**
* Convert a latitude and longitude into an OSGB grid reference
*
* @param latitudeLongitude the latitude and longitude to convert
* @return the OSGB grid reference
* @since 0.1
*/
function LatLngToOSRef(lati, lngi) {
    var airy1830 = new RefEll(6377563.396, 6356256.909);
    var OSGB_F0 = 0.9996012717;
    var N0 = -100000.0;
    var E0 = 400000.0;
    var phi0 = deg2rad(49.0);
    var lambda0 = deg2rad(-2.0);
    var a = airy1830.maj;
    var b = airy1830.min;
    var eSquared = airy1830.ecc;
    var phi = deg2rad(lati);
    var lambda = deg2rad(lngi);
    var E = 0.0;
    var N = 0.0;
    var n = (a - b) / (a + b);
    var v = a * OSGB_F0 * Math.pow(1.0 - eSquared * sinSquared(phi), -0.5);
    var rho =
    a * OSGB_F0 * (1.0 - eSquared) * Math.pow(1.0 - eSquared * sinSquared(phi), -1.5);
    var etaSquared = (v / rho) - 1.0;
    var M =
    (b * OSGB_F0)
      * (((1 + n + ((5.0 / 4.0) * n * n) + ((5.0 / 4.0) * n * n * n))
        * (phi - phi0))
        - (((3 * n) + (3 * n * n) + ((21.0 / 8.0) * n * n * n))
          * Math.sin(phi - phi0)
          * Math.cos(phi + phi0))
        + ((((15.0 / 8.0) * n * n) + ((15.0 / 8.0) * n * n * n))
          * Math.sin(2.0 * (phi - phi0))
          * Math.cos(2.0 * (phi + phi0)))
        - (((35.0 / 24.0) * n * n * n)
          * Math.sin(3.0 * (phi - phi0))
          * Math.cos(3.0 * (phi + phi0))));
    var I = M + N0;
    var II = (v / 2.0) * Math.sin(phi) * Math.cos(phi);
    var III =
    (v / 24.0)
      * Math.sin(phi)
      * Math.pow(Math.cos(phi), 3.0)
      * (5.0 - tanSquared(phi) + (9.0 * etaSquared));
    var IIIA =
    (v / 720.0)
      * Math.sin(phi)
      * Math.pow(Math.cos(phi), 5.0)
      * (61.0 - (58.0 * tanSquared(phi)) + Math.pow(Math.tan(phi), 4.0));
    var IV = v * Math.cos(phi);
    var V = (v / 6.0) * Math.pow(Math.cos(phi), 3.0) * ((v / rho) - tanSquared(phi));
    var VI =
    (v / 120.0)
      * Math.pow(Math.cos(phi), 5.0)
      * (5.0
        - (18.0 * tanSquared(phi))
        + (Math.pow(Math.tan(phi), 4.0))
        + (14 * etaSquared)
        - (58 * tanSquared(phi) * etaSquared));

    N =
    I
      + (II * Math.pow(lambda - lambda0, 2.0))
      + (III * Math.pow(lambda - lambda0, 4.0))
      + (IIIA * Math.pow(lambda - lambda0, 6.0));
    E =
    E0
      + (IV * (lambda - lambda0))
      + (V * Math.pow(lambda - lambda0, 3.0))
      + (VI * Math.pow(lambda - lambda0, 5.0));

    return new OSRef(E, N);
}


/**
* Convert an UTM reference to a latitude and longitude
*
* @param ellipsoid A reference ellipsoid to use
* @param utm the UTM reference to convert
* @return the converted latitude and longitude
* @since 0.2
*/
function UTMRefToLatLng() {
    var wgs84 = new RefEll(6378137, 6356752.314);
    var UTM_F0 = 0.9996;
    var a = wgs84.maj;
    var eSquared = wgs84.ecc;
    var ePrimeSquared = eSquared / (1.0 - eSquared);
    var e1 = (1 - Math.sqrt(1 - eSquared)) / (1 + Math.sqrt(1 - eSquared));
    var x = this.easting - 500000.0; ;
    var y = this.northing;
    var zoneNumber = this.lngZone;
    var zoneLetter = this.latZone;

    var longitudeOrigin = (zoneNumber - 1.0) * 6.0 - 180.0 + 3.0;

    // Correct y for southern hemisphere
    if ((ord(zoneLetter) - ord("N")) < 0) {
        y -= 10000000.0;
    }

    var m = y / UTM_F0;
    var mu =
    m
      / (a
        * (1.0
          - eSquared / 4.0
          - 3.0 * eSquared * eSquared / 64.0
          - 5.0
            * Math.pow(eSquared, 3.0)
            / 256.0));

    var phi1Rad =
    mu
      + (3.0 * e1 / 2.0 - 27.0 * Math.pow(e1, 3.0) / 32.0) * Math.sin(2.0 * mu)
      + (21.0 * e1 * e1 / 16.0 - 55.0 * Math.pow(e1, 4.0) / 32.0)
        * Math.sin(4.0 * mu)
      + (151.0 * Math.pow(e1, 3.0) / 96.0) * Math.sin(6.0 * mu);

    var n =
    a
      / Math.sqrt(1.0 - eSquared * Math.sin(phi1Rad) * Math.sin(phi1Rad));
    var t = Math.tan(phi1Rad) * Math.tan(phi1Rad);
    var c = ePrimeSquared * Math.cos(phi1Rad) * Math.cos(phi1Rad);
    var r =
    a
      * (1.0 - eSquared)
      / Math.pow(
        1.0 - eSquared * Math.sin(phi1Rad) * Math.sin(phi1Rad),
        1.5);
    var d = x / (n * UTM_F0);

    var latitude = (
    phi1Rad
      - (n * Math.tan(phi1Rad) / r)
        * (d * d / 2.0
          - (5.0
            + (3.0 * t)
            + (10.0 * c)
            - (4.0 * c * c)
            - (9.0 * ePrimeSquared))
            * Math.pow(d, 4.0)
            / 24.0
          + (61.0
            + (90.0 * t)
            + (298.0 * c)
            + (45.0 * t * t)
            - (252.0 * ePrimeSquared)
            - (3.0 * c * c))
            * Math.pow(d, 6.0)
            / 720.0)) * (180.0 / Math.PI);

    var longitude = longitudeOrigin + (
    (d
      - (1.0 + 2.0 * t + c) * Math.pow(d, 3.0) / 6.0
      + (5.0
        - (2.0 * c)
        + (28.0 * t)
        - (3.0 * c * c)
        + (8.0 * ePrimeSquared)
        + (24.0 * t * t))
        * Math.pow(d, 5.0)
        / 120.0)
      / Math.cos(phi1Rad)) * (180.0 / Math.PI);

    return new LatLng(latitude, longitude);
}


/**
* Convert a latitude and longitude to an UTM reference
*
* @param ellipsoid A reference ellipsoid to use
* @param latitudeLongitude The latitude and longitude to convert
* @return the converted UTM reference
* @since 0.2
*/
function LatLngToUTMRef() {
    var wgs84 = new RefEll(6378137, 6356752.314);
    var UTM_F0 = 0.9996;
    var a = wgs84.maj;
    var eSquared = wgs84.ecc;
    var longitude = this.lng;
    var latitude = this.lat;

    var latitudeRad = latitude * (Math.PI / 180.0);
    var longitudeRad = longitude * (Math.PI / 180.0);
    var longitudeZone = Math.floor((longitude + 180.0) / 6.0) + 1;

    // Special zone for Norway
    if (latitude >= 56.0
    && latitude < 64.0
    && longitude >= 3.0
    && longitude < 12.0) {
        longitudeZone = 32;
    }

    // Special zones for Svalbard
    if (latitude >= 72.0 && latitude < 84.0) {
        if (longitude >= 0.0 && longitude < 9.0) {
            longitudeZone = 31;
        } else if (longitude >= 9.0 && longitude < 21.0) {
            longitudeZone = 33;
        } else if (longitude >= 21.0 && longitude < 33.0) {
            longitudeZone = 35;
        } else if (longitude >= 33.0 && longitude < 42.0) {
            longitudeZone = 37;
        }
    }

    var longitudeOrigin = (longitudeZone - 1) * 6 - 180 + 3;
    var longitudeOriginRad = longitudeOrigin * (Math.PI / 180.0);

    var UTMZone = getUTMLatitudeZoneLetter(latitude);

    ePrimeSquared = (eSquared) / (1 - eSquared);

    var n = a / Math.sqrt(1 - eSquared * Math.sin(latitudeRad) * Math.sin(latitudeRad));
    var t = Math.tan(latitudeRad) * Math.tan(latitudeRad);
    var c = ePrimeSquared * Math.cos(latitudeRad) * Math.cos(latitudeRad);
    var A = Math.cos(latitudeRad) * (longitudeRad - longitudeOriginRad);

    var M =
    a
      * ((1
        - eSquared / 4
        - 3 * eSquared * eSquared / 64
        - 5 * eSquared * eSquared * eSquared / 256)
        * latitudeRad
        - (3 * eSquared / 8
          + 3 * eSquared * eSquared / 32
          + 45 * eSquared * eSquared * eSquared / 1024)
          * Math.sin(2 * latitudeRad)
        + (15 * eSquared * eSquared / 256
          + 45 * eSquared * eSquared * eSquared / 1024)
          * Math.sin(4 * latitudeRad)
        - (35 * eSquared * eSquared * eSquared / 3072)
          * Math.sin(6 * latitudeRad));

    var UTMEasting =
    (UTM_F0
      * n
      * (A
        + (1 - t + c) * Math.pow(A, 3.0) / 6
        + (5 - 18 * t + t * t + 72 * c - 58 * ePrimeSquared)
          * Math.pow(A, 5.0)
          / 120)
      + 500000.0);

    var UTMNorthing =
    (UTM_F0
      * (M
        + n
          * Math.tan(latitudeRad)
          * (A * A / 2
            + (5 - t + (9 * c) + (4 * c * c)) * Math.pow(A, 4.0) / 24
            + (61 - (58 * t) + (t * t) + (600 * c) - (330 * ePrimeSquared))
              * Math.pow(A, 6.0)
              / 720)));

    // Adjust for the southern hemisphere
    if (latitude < 0) {
        UTMNorthing += 10000000.0;
    }

    return new UTMRef(UTMEasting, UTMNorthing, UTMZone, longitudeZone);
}

/**
* Take a string formatted as a six-figure OS grid reference (e.g.
* "TG514131") and return a reference to an OSRef object that represents
* that grid reference. The first character must be H, N, S, O or T.
* The second character can be any uppercase character from A through Z
* excluding I.
*
* @param ref
* @return
* @since 1.1
*/
function getOSRefFromSixFigureReference(ref) {
    var char1 = ref.substring(0, 1);
    var char2 = ref.substring(1, 2);
    // Thanks to Nick Holloway for pointing out the radix bug here
    var east = parseInt(ref.substring(2, 5), 10) * 100;
    var north = parseInt(ref.substring(5, 8), 10) * 100;
    if (char1 == 'H') {
        north += 1000000;
    } else if (char1 == 'N') {
        north += 500000;
    } else if (char1 == 'O') {
        north += 500000;
        east += 500000;
    } else if (char1 == 'T') {
        east += 500000;
    }
    var char2ord = ord(char2);
    if (char2ord > 73) char2ord--; // Adjust for no I
    var nx = ((char2ord - 65) % 5) * 100000;
    var ny = (4 - Math.floor((char2ord - 65) / 5)) * 100000;
    return new OSRef(east + nx, north + ny);
}


/**
*  Work out the UTM latitude zone from the latitude
*
* @param latitude
* @return
* @since 0.2
*/
function getUTMLatitudeZoneLetter(latitude) {
    if ((84 >= latitude) && (latitude >= 72)) return "X";
    else if ((72 > latitude) && (latitude >= 64)) return "W";
    else if ((64 > latitude) && (latitude >= 56)) return "V";
    else if ((56 > latitude) && (latitude >= 48)) return "U";
    else if ((48 > latitude) && (latitude >= 40)) return "T";
    else if ((40 > latitude) && (latitude >= 32)) return "S";
    else if ((32 > latitude) && (latitude >= 24)) return "R";
    else if ((24 > latitude) && (latitude >= 16)) return "Q";
    else if ((16 > latitude) && (latitude >= 8)) return "P";
    else if ((8 > latitude) && (latitude >= 0)) return "N";
    else if ((0 > latitude) && (latitude >= -8)) return "M";
    else if ((-8 > latitude) && (latitude >= -16)) return "L";
    else if ((-16 > latitude) && (latitude >= -24)) return "K";
    else if ((-24 > latitude) && (latitude >= -32)) return "J";
    else if ((-32 > latitude) && (latitude >= -40)) return "H";
    else if ((-40 > latitude) && (latitude >= -48)) return "G";
    else if ((-48 > latitude) && (latitude >= -56)) return "F";
    else if ((-56 > latitude) && (latitude >= -64)) return "E";
    else if ((-64 > latitude) && (latitude >= -72)) return "D";
    else if ((-72 > latitude) && (latitude >= -80)) return "C";
    else return 'Z';
}