﻿/*
 *  GeoUtil.js
 *
 *  A collection of JavaScript objects and methods
 *  relating to geo-mapping and the Google Maps API
 *
 *  Requires:
 *   AjaxUtil.js : used to make Ajax requests
 *
 *  HowTo:
 *   To add a Google Map to a page there needs to be a separate div for the map (called map canvas)
 *   and for the status (called map status). These div's need to have the css class "map_canvas" and
 *   "map_status", respectively; the css file "/Common/GoogleMaps.css" needs to be included.
 *
 *   The first JS call to make once the page is loaded is GeoUtil.Initializemap; this function actually
 *   creates the map, adds the normal controls, and sets the body's unload event (as required by Google).
 *   Once complete, the map is fully functional and other JS calls can be made to manipulate the map
 *   as needed.
 */

// The GeoUtil object that defines the GeoUtil namespace
var GeoUtil = new Object();
GeoUtil.Private = new Object(); // Second namespace for private functions and variables

GeoUtil.ClickCallBack = function(){}
GeoUtil.FIND_NON_NUMERIC = /[^\d]/g;
GeoUtil.GEOCODE_AUTO = 0;
GeoUtil.GEOCODE_CUSTOMER = 2;
GeoUtil.GEOCODE_MAINTENANCE = 1;
GeoUtil.geocoder = null;        // GClientGeocoder object used to geocode addresses
GeoUtil.GeocoderCallback;       // Used to store the CallBack function when geocoding an address
GeoUtil.map;
GeoUtil.MarkerListItemClass = null;
GeoUtil.MarkerListTitle = 'Available Markers';
GeoUtil.MarkerListTooltip = 'markers';
GeoUtil.MarkerListMultipleIcon = '/images/markerMultiple2.png';
GeoUtil.Private.ClickEventAdded = false;
GeoUtil.Private.ManagedMarkers  // Array that stores managed markers
GeoUtil.radix = 10;   // Radix used for parseInt
GeoUtil.zoomLevel = new Array(2,4,6,10,12,13,16,16,17);   // This array maps geocode accuracies to Google Map zoom levels

// The default ID's for the div tag containing the status and canvas, respectively; these can be overriden
GeoUtil.mapStatusId = "map_status";
GeoUtil.mapCanvasId = "map_canvas";

GeoUtil.InitializeMap = function(CallBack){
/*
 * Function to create a Google map on the page.  The div with id _MapCanvasId is used
 *  to hold the map's contents. If this div does not have its height and width fixed,
 *  the map will expand to fill the browser.
 *
 * Input:
 *  callbackFunction : the function that will be called when the map has been initialized
 *
 * Output:
 *  None
 */

    if(GBrowserIsCompatible()){
        // Display the map, with some controls and set the initial location
        GeoUtil.map = new GMap2(document.getElementById(GeoUtil.mapCanvasId));

        // Add map controllers
        //GeoUtil.map.addControl(new GLargeMapControl());
        GeoUtil.map.addControl(new GMapTypeControl());
        GeoUtil.map.addControl(new GLargeMapControl3D());
    }

    // Override the existing clearOverlays function
    GeoUtil.map.clearOverlaysCopy = GeoUtil.map.clearOverlays;
    GeoUtil.map.clearOverlays = function(){
        GeoUtil.Private.ManagedMarkers = null;
        GeoUtil.Private.ManagedMarkers = new Array();

        // Run original version
        GeoUtil.map.clearOverlaysCopy();
    }

    // Set body unload event
    document.body.onunload = GUnload;


    // Call callback function if defined
    if(CallBack != undefined){
        CallBack();
    }
}

GeoUtil.GetGeocodeClient = function(searchQuery, CallBack){
/*
 * Function to initiate the Ajax request to lookup a geocode
 *
 * Input:
 *  searchQuery : The query passed to Google
 *  CallBack    : Function to call with response from Ajax request
 *
 * Output:
 *  None
 */
    var targetUrl = '/Resources/Geocoder.aspx?q=' + searchQuery;

    // Make sure the object has been previously created.  If not, create it.
    if(GeoUtil.geocoder == null){
        GeoUtil.geocoder = new GClientGeocoder();
    }

    // Save the CallBack function so ProcessGeocodeClient may use it later
    GeoUtil.GeocodeCallBack = CallBack;

    // Make the request
    GeoUtil.geocoder.getLocations(searchQuery, GeoUtil.ProcessGeocodeClient);
}

GeoUtil.ProcessGeocodeClient = function(result){
/**
 * Function to process the geocode lookup response from Google. It extracts
 * the search result data and creates a Geocode object to return to the
 * CallBack function.
 *
 * Input:
 *  result    : a JavaScript object
 *
 * Output:
 *  a geocode object
 */
    var GEOCODE_STATUS_SUCCESS = 200;
    var geocode = new GeoUtil.Geocode();

    // If the geocode request was not successful, leave the default geocode values in place
    if(result.Status.code == GEOCODE_STATUS_SUCCESS && result.Placemark && result.Placemark.length > 0){
        geocode.lat = result.Placemark[0].Point.coordinates[1];
        geocode.lng = result.Placemark[0].Point.coordinates[0];
        geocode.acc = result.Placemark[0].AddressDetails.Accuracy;
    }

    GeoUtil.GeocodeCallBack(geocode);
}

GeoUtil.GetGeocodeServer = function(searchQuery, CallBack){
/**
 * Function to initiate a request to lookup a geocode using the server as opposed to the client
 *
 * Input:
 *  searchQuery : the address to be geocoded
 *  CallBack    : the function to be called with the resulting geocode
 *
 * Output:
 *  None, but the processing function (ProcessGeocodeServer) returns the resulting geocode
 *
 * Notes:
 *  Works in conjunction with ProcessGeocodeServer, which processes the request
 */

    // Save the CallBack function to ProcessGeocodeServer may use it later
    GeoUtil.GeocodeCallBack = CallBack;

    AjaxUtil.MakeRequest('/Resources/geocoder.aspx?q=' + searchQuery, GeoUtil.ProcessGeocodeServer);
}

GeoUtil.ProcessGeocodeServer = function(httpRequest){
/**
 * Function to process the request to lookup a geocode using the server as opposed to the client
 *
 * Input:
 *  httpRequest : the request object with the resulting data
 *
 * Output:
 *  The resulting geocode object
 *
 * Notes:
 *  Works in conjunction with GetGeocodeServer, which initiates the request
 */
    var geocode = new GeoUtil.Geocode();
    var jsonData = eval('(' + httpRequest.responseText + ')');

    geocode.lat = parseFloat(jsonData.latitude);
    geocode.lng = parseFloat(jsonData.longitude);
    geocode.acc = parseInt(jsonData.accuracy, GeoUtil.radix);
    geocode.accDesc = jsonData.accuracyDesc;

    if(isNaN(geocode.lat) || isNaN(geocode.lng) || isNaN(geocode.acc)){
        geocode = new GeoUtil.Geocode();
    }

    GeoUtil.GeocodeCallBack(geocode);
}

GeoUtil.CenterMap = function(Center){
/*
 * Function to center the Google map on the given location
 *
 * Input
 *  Center : geocode object (lat, lng, acc)
 *
 * Output:
 *  None
 */
    GeoUtil.map.setCenter(new GLatLng(Center.lat, Center.lng), GeoUtil.zoomLevel[Center.acc]);
}

GeoUtil.GetMapCoords = function(){
/*
 * Function that extracts the current coordinates of the Google map
 *
 * Input:
 *  None
 *
 * Output:
 *  region : geocodeRect object (upperLeft, bottomRight) that defines the region of the map
 */
    var mapBounds = GeoUtil.map.getBounds();
    var southWest = mapBounds.getSouthWest();
    var northEast = mapBounds.getNorthEast();
    var region = new GeoUtil.GeocodeRect();

    // Translates the defining points as returned from Google into (upperLeft, bottomRight) format
    //  because Google stores these points as (bottomLeft, upperRight) internally.
    region.UpperLeft.lat = northEast.lat();
    region.UpperLeft.lng = southWest.lng();
    region.BottomRight.lat = southWest.lat();
    region.BottomRight.lng = northEast.lng();

    return(region);
}

GeoUtil.CreateMarker = function(Location, name, id, markerImageUrl){
/*
 * Function to create a marker at the given point on a Google map
 *
 * Input
 *  Location        : the geocode object for the location of the marker
 *  name            : the textual tooltip for the marker
 *  id              : the unique id to be set at the vtId property of the marker
 *  markerImageUrl  : the location of the image to use as a marker
 *
 * Output
 *  None
 */
    var marker = GeoUtil.Private.CreateMarker(Location, name, id, markerImageUrl);

    GeoUtil.map.addOverlay(marker);
}

GeoUtil.Private.CreateMarker = function(Location, name, id, markerImageUrl){
    var googleLocation = new GLatLng(Location.lat, Location.lng);
    var marker;

    // If a marker image was passed along, use it
    if(markerImageUrl != undefined){
        var icon = new GIcon();
        icon.image = markerImageUrl;
        icon.shadow = "";
        icon.iconSize = new GSize(20, 34);
        icon.iconAnchor = new GPoint(10, 34);
        icon.infoWindowAnchor = new GPoint(5, 1);

        marker = new GMarker(googleLocation, {title:name,icon: icon});
    } else {
        marker =  new GMarker(googleLocation, {title:name});
    }

    // If the marker object does not contain the vtId property, add it
    if(GMarker.prototype.vtId == null){
        GMarker.prototype.vtId = new Number();
    }

    if(GMarker.prototype.vtTitle == null){
        GMarker.prototype.vtTitle = new String();
    }

    if(GMarker.prototype.vtLocation == null){
        GMarker.prototype.vtLocation = new GeoUtil.Geocode();
    }

    // Set the marker id
    if(id != undefined){
        marker.vtId = id;
    }

    // Set the marker title
    if(name != undefined){
        marker.vtTitle = name;
    }

    marker.vtLocation = Location;

    return marker;
}

GeoUtil.QueueManagedMarker = function(Location, name, id, markerImageUrl, customProperties){
/*
 * Function to create a "managed" marker at the given point on a Google map.
 * Unlike the CreateMarker function, this does not add the marker to the map,
 * but simply queues the marker to be added later. A subsequent call to
 * GeoUtil.ShowManagedMarkers() must be made to add all queued markers to
 * the map.
 *
 * Input
 *  Location         : the geocode object for the location of the marker
 *  name             : the textual tooltip for the marker
 *  id               : the unique id to be set at the vtId property of the marker
 *  markerImageUrl   : the location of the image to use as a marker
 *  customProperties : (optional) a JavaScript object that will be available via marker.vtCustom
 *                     whenever this marker is passed to a callback function. If the calling
 *                     method needs any custom properties not natively supported by the GeoUtil
 *                     class, those properties can be passed in here in the form of an object.
 *
 * Output
 *  None
 */
    var marker = GeoUtil.Private.CreateMarker(Location, name, id, markerImageUrl);

    // Add custom properties to marker object
    if(customProperties && customProperties.constructor == Object){
        var index;

        marker.vtCustom = new Object();

        for(index in customProperties){
            marker.vtCustom[index] = customProperties[index];
        }
    }

    // Initialize array of managed markers, if necessary
    if(GeoUtil.Private.ManagedMarkers == undefined){
        GeoUtil.Private.ManagedMarkers = new Array();
    }

    // Add marker to collection of managed markers
    if(GeoUtil.Private.ManagedMarkers[Location.Print()] != undefined){
        GeoUtil.Private.ManagedMarkers[Location.Print()].push(marker);
    } else {
        GeoUtil.Private.ManagedMarkers[Location.Print()] = new Array(marker);
    }
}

GeoUtil.ShowManagedMarkers = function(){
/*
 * Function to show any markers previously queued with QueueManagedMarker
 * on the map. If two markers happen to contain the same Location, a list
 * will be displayed with links to activating each marker individually.
 *
 * Input
 *  None, but markers should have been previously queued.
 *
 * Output
 *  Markers on the map
 */
    for(listKey in GeoUtil.Private.ManagedMarkers){
        var currentList = GeoUtil.Private.ManagedMarkers[listKey];

        if(currentList.length == 1){
            GeoUtil.map.addOverlay(currentList[0]);
        } else if(currentList.length > 1){
            var marker = GeoUtil.Private.CreateMarker(currentList[0].vtLocation, currentList.length + ' ' + GeoUtil.MarkerListTooltip, null, GeoUtil.MarkerListMultipleIcon);

            GeoUtil.map.addOverlay(marker);
        }

        if(!GeoUtil.Private.ClickEventAdded){
            GEvent.addListener(GeoUtil.map, "click", function(overlay, point){
                if(overlay){
                    if(overlay.vtId != undefined && overlay.vtId != null){
                        GeoUtil.currentOverlay = overlay;

                        if(GeoUtil.Private.ManagedMarkers[overlay.vtLocation.Print()].length == 1){
                            GeoUtil.ClickCallBack(overlay, point);
                        } else if(GeoUtil.Private.ManagedMarkers[overlay.vtLocation.Print()].length > 1){
                            var infoHtml = '';

                            // Build list of markers
                            infoHtml += '<div class="markerList"><h3>' + GeoUtil.MarkerListTitle + '</h3><ul>';
                            for(markerIndex in GeoUtil.Private.ManagedMarkers[overlay.vtLocation.Print()]){
                                var marker = GeoUtil.Private.ManagedMarkers[overlay.vtLocation.Print()][markerIndex];
                                var title = 'Untitled Marker';

                                if(marker.vtTitle != null && marker.vtTitle.length > 0){
                                    title = marker.vtTitle;
                                }

                                // User appropriate class for the list item
                                if(GeoUtil.MarkerListItemClass != null){
                                    if(typeof(GeoUtil.MarkerListItemClass) == 'function'){
                                        infoHtml += '<li class="' + GeoUtil.MarkerListItemClass(marker) + '">';
                                    } else {
                                        infoHtml += '<li class="' + GeoUtil.MarkerListItemClass + '">';
                                    }
                                } else {
                                    infoHtml += '<li>';
                                }
                                infoHtml += '<a href="javascript: void(0);" onclick=\'GeoUtil.Private.ProcessMarkerDetail(GeoUtil.Private.ManagedMarkers["' + marker.vtLocation.Print() + '"][' + markerIndex + ']);\'>' + title + '</a></li>';
                            }
                            infoHtml += '</ul></div>';

                            GeoUtil.Private.CurrentListOverlay = overlay;
                            overlay.openInfoWindow(infoHtml);
                        }
                    }
                }
            });
            GeoUtil.Private.ClickEventAdded = true;
        }
    }
}

GeoUtil.Private.ProcessMarkerDetail = function(marker){
    GeoUtil.Private.CurrentListOverlay.vtId = marker.vtId;
    GeoUtil.Private.CurrentListOverlay.vtTitle = marker.vtTitle;

    // If any custom properties were given when the marker was originally queued
    // those properties must be copied into the current overlay marker. Since the
    // original marker is never actually added to the map as an overlay, we can't
    // pass that marker to the callback; otherwise, the callback would never be
    // able to launch an info window.
    if(marker.vtCustom){
      GeoUtil.Private.CurrentListOverlay.vtCustom = marker.vtCustom;
    }

    GeoUtil.ClickCallBack(GeoUtil.Private.CurrentListOverlay, null);
}

GeoUtil.SetMapCanvasId = function(newId){
/**
 * Function that allows the map canvas ID to be changed
 *
 * Input:
 *  newId : the new ID to be used for the map canvas
 *
 * Output:
 *  None
 */
    GeoUtil.mapCanvasId = newId;
}
GeoUtil.SetMapStatusId = function(newId){
/**
 * Function that allows the map status ID to be changed
 *
 * Input:
 *  newId : the new ID to be used for the map status
 *
 * Output:
 *  None
 */
    GeoUtil.mapStatusId = newId;
}

GeoUtil.Geocode = function(lat, lng, acc){
/*
 * Object that defines a geographical location; similar to the GLatLng object defined
 *  by the Goolge Maps API.
 *
 * Members
 *  lat : the latitude of the location
 *  lng : the longitude of the location
 *  acc : the estimated accuracy of lat, lng representation of the location
 *
 */
    if(lat == undefined){
        this.lat = 0;
    } else {
        this.lat = lat;
    }

    if(lng == undefined){
        this.lng = 0;
    } else {
        this.lng = lng;
    }

    if(acc == undefined){
        this.acc = 0;
    } else {
        this.acc = acc;
    }

    this.Print = function(){
        return(this.lat + ', ' + this.lng);
    }

    this.accDesc = '';
    this.type = 'Geocode';
}

GeoUtil.GeocodeRect = function(UpperLeft, BottomRight){
/*
 * Object that defines a rectangular geographic region consisting of two geocode objects
 *
 * Members
 *  upperLeft   : the location of the upper, left coordinate that defines the region
 *  bottomRight : the location of the bottom, right coordinate that defines the region
 *
 */
    if(UpperLeft == undefined){
        this.UpperLeft = new GeoUtil.Geocode();
    } else {
        this.UpperLeft = UpperLeft;
    }

    if(BottomRight == undefined){
        this.BottomRight = new GeoUtil.Geocode();
    } else {
        this.BottomRight = BottomRight;
    }

    this.Print = function(){
        return('(' + this.UpperLeft.Print() + '),(' + this.BottomRight.Print() + ')');
    }
}

GeoUtil.DisplayMapMessage = function(message){
/*
 * Function to display a given message on the Google map
 *
 * Input
 *  message : the textual/html message to be displayed
 *
 * Output
 *  None
 */
    var mapStatus = document.getElementById(GeoUtil.mapStatusId);

    mapStatus.innerHTML = message;
    mapStatus.style.visibility = "visible";
}

GeoUtil.RemoveMapMessage = function(){
/*
 * Function to remove any message on the Google map
 *
 * Input:
 *  None
 *
 * Output:
 *  None
 */
    var mapStatus = document.getElementById(GeoUtil.mapStatusId);

    mapStatus.style.visibility = "hidden";
}

GeoUtil.ShowMapByAddress = function(pointAddress){
/*
 * Function to show a Google Maps page with the specified address
 *
 * Input:
 *  pointAddress : the address to be mapped by Google
 *
 * Output:
 *  Map with Google Map
 */
    window.open('http://maps.google.com/maps?q=' + pointAddress.replace(/#/, ""),'showMap','width=' + 650 + ',height=' + 600 + ',left=' + ((screen.availWidth - 650 - 10) * .5) + ',scrollbars=yes,menubar=no,resizable=yes,top=' + ((screen.availHeight - 600 - 30) * .5));
}

GeoUtil.ShowMapByGeocode = function(Point, title, addr1, addr2, dir, edit, auto){
/*
 * Function to show a Google Maps page with the specified location (geocode)
 *
 * Input:
 *  point         : the geocode object of the location to be mapped by Google
 *  title         : the text to appear in the title of the pop-up window
 *  addr1         : the text to appear in the 1st line of the marker's info window
 *                  if addr1 is provided, but title is blank, addr1 will be used as title
 *  addr2         : the text to appear in the 2nd line of the marker's info window
 *  dir           : boolean that specifies whether or not to show links for driving directions
 *  edit          : boolean that specifies whether or not to show a link to edit the marker's location
 *  auto          : boolean that specifies whether or not to auto-display the marker's info window
 *
 * Output:
 *  Map window
 *
 * Note:
 *  All fields are required.
 *
 */
    var launchWindow = true;
    var targetUrl = '/geomap.htm';
    var params = '?';

    // Check that the correct number of arguments was specified
    if(arguments.length < 7){
      launchWindow = false;
    }

    // Add geocode location to url
    if(typeof(Point) == 'object' && Point.type == 'Geocode'){
      params += 'lat=' + Point.lat + '&lng=' + Point.lng;
    } else {
      launchWindow = false;
    }

    // Add window title
    if(title.toString().length > 0){
      params += '&title=' + encodeURIComponent(title.toString());
    }

    // Add addr1
    if(addr1.toString().length > 0){
      params += '&addr1=' + encodeURIComponent(addr1.toString());
    }

    // Add addr2
    if(addr2.toString().length > 0){
      params += '&addr2=' + encodeURIComponent(addr2.toString());
    }

    // Add dir
    if(typeof(dir) == 'boolean' && dir == true){
      params += '&dir=true';
    }

    // Add edit
    if(typeof(edit) == 'boolean' && edit == true){
      params += '&edit=true';
    }

    // Add auto
    if(typeof(auto) == 'boolean' && auto == true){
      params += '&auto=true';
    }

    // If no errors were encountered, launch window
    if(launchWindow){
      window.open(targetUrl + params, '', 'width=' + 750 + ',height=' + 600 + ',left=' + ((screen.availWidth - 750 - 10) * .5) + ',scrollbars=yes,menubar=no,resizable=yes,top=' + ((screen.availHeight - 600 - 30) * .5));
    }
}

GeoUtil.AssembleFullAddress = function(street, city, state, zip, country){
/**
 * Function to assemble a full address from it's components
 *
 * Input:
 *  street, city, state, zip, country
 *
 * Output:
 *  the full address
 */
    var elements = [street, city, state, zip, country];

    return(elements.join("|"));

    // build address into a single string
//    if(street != undefined && street.length > 0){
//      fullAddress = street;
//    }
//    if(city != undefined && city.length > 0){
//      if(fullAddress.length > 0){ fullAddress += ','; }
//      fullAddress += city;
//    }
//    if(state != undefined && state.length > 0){
//      if(fullAddress.length > 0){ fullAddress += ','; }
//      fullAddress += state;
//    }
//    if(zip != undefined && zip.length > 0){
//      if(fullAddress.length > 0){ fullAddress += ' '; }
//      fullAddress += zip;
//    }
//    if(country != undefined && country.length > 0){
//      if(fullAddress.length > 0){ fullAddress += ','; }
//      fullAddress += country;
//    }
//
//    return(fullAddress);
}

GeoUtil.GetDestinationPoint = function(location, radius, bearing){
    var earthRadius = 6371; // Earth's mean radius in km
    var resultPoint = new GeoUtil.Geocode();
    var centerPoint = new GeoUtil.Geocode();

    centerPoint.lat = GeoUtil.DegreesToRadians(location.lat);
    centerPoint.lng = GeoUtil.DegreesToRadians(location.lng);
    bearing = GeoUtil.DegreesToRadians(bearing);
    radius = GeoUtil.MilesToKilometers(radius);

    resultPoint.lat = Math.asin( Math.sin(centerPoint.lat)*Math.cos(radius/earthRadius) +
                                 Math.cos(centerPoint.lat)*Math.sin(radius/earthRadius)*Math.cos(bearing));
    resultPoint.lng = centerPoint.lng + Math.atan2( Math.sin(bearing)*Math.sin(radius/earthRadius)*Math.cos(centerPoint.lat),
                                                    Math.cos(radius/earthRadius)-Math.sin(centerPoint.lat)*Math.sin(resultPoint.lat));

    resultPoint.lng = (resultPoint.lng + Math.PI)%(2*Math.PI) - Math.PI;    // Normalize to -180...+180

    resultPoint.lat = GeoUtil.RadiansToDegrees(resultPoint.lat);
    resultPoint.lng = GeoUtil.RadiansToDegrees(resultPoint.lng);

    return(resultPoint);
}

/*
 *  Generic Conversion Functions
 */
GeoUtil.DegreesToRadians = function(inDegrees){
    return((inDegrees*Math.PI)/180);
}

GeoUtil.RadiansToDegrees = function(inRadians){
    return((inRadians*180)/Math.PI);
}

GeoUtil.MilesToKilometers = function(inMiles){
    return(inMiles*1.609344);
}

GeoUtil.KilometersToMiles = function(inKilometers){
    return(inKilometers*0.6213711);
}

