﻿/* Begin MinifyOption: Defer */
// you might have thought that just checking window['google'] would be enough, but not for google chrome...
if (window.google != undefined && google.load != undefined) {

	google.load('maps', '2.x');
	f.AttachEvent(window, 'unload', function () { google.maps.Unload(); });

};
/* End MinifyOption */

var GoogleMap = function (oMap, nLat, nLong, oZoomEndDelegate, oDragEndDelegate, bShowGoogleEarth) {


    /* 
    Contents 
			
				
    1. Map General
    1.1 Properties
    1.2 Setup
    1.3 Class Constructor
			
    2. Icons
    2.1 Add Icon
    2.2 Clear Icons
			
    3. Markers
    3.1 Add Marker
    3.2 Remove Marker
    3.3 Clear Markers
			
    4. Utility Functions
    4.1 Build
    4.2 Remove Outliers
    4.3 Cluster
	
    5. Support
    5.1 Safe Marker
    5.2 Centre And Zoom
    5.3 Zoom To Marker
    5.4 Bounds
    5.5 Get Marker Coordinates
    5.6 Get Marker Screen Position
    5.7 Calculate Mean Geographical Coordinates
			
    */




    /* 1 Map General */

    // 1.1 Properties
    var me = this;
    this.MapObject;

    this.Map;
    this.Markers = new Object();
    this.Icons = new Object();

    this.ManualZoom = false;

    this.OriginalBounds;


    // 1.2 Setup
    this.Setup = function (oMap, nLat, nLong, oZoomEndDelegate, oDragEndDelegate, bShowGoogleEarth) {

        this.MapObject = f.SafeObject(oMap);
        if (this.MapObject == null && window.location.toString().indexOf('localhost') > -1) {
            alert('Please debug me: Could not set up the map correctly - could not get the requested map object');
            return;
        };
        
        me.Map = new google.maps.Map2(this.MapObject);

        if (bShowGoogleEarth) {
            me.Map.addMapType(google.maps.SATELLITE_3D_MAP);
        };

        if (nLat != undefined) {
            me.Map.setCenter(new google.maps.LatLng(nLat, nLong), 13);
        } else {
            me.Map.setCenter(new google.maps.LatLng(0, 0), 1);
        };
        me.Map.setUIToDefault();

        // if there is a zoom end delegate then tie up the event to a listener
        if (oZoomEndDelegate != undefined) {
            GEvent.addListener(me.Map, "zoomend", oZoomEndDelegate);
        };

        // if there is a drag end delegate then tie up the event to a listener
        if (oDragEndDelegate != undefined) {
            GEvent.addListener(me.Map, "dragend", oDragEndDelegate);
        };

    };


    // 1.3 Class Constructor
    me.Setup(oMap, nLat, nLong, oZoomEndDelegate, oDragEndDelegate, bShowGoogleEarth);




    /* 2 Icons */

    // 2.1 Add Icon
    this.AddIcon = function (sIconKey, sURL, iWidth, iHeight, iAnchorX, iAnchorY) {
        var oIcon = { URL: sURL, Width: iWidth, Height: iHeight, AnchorX: iAnchorX, AnchorY: iAnchorY, ParentMap: me };
        me.Icons[sIconKey] = oIcon;
        return oIcon;
    };


    // 2.2 Clear Icons
    this.ClearIcons = function () {
        me.Icons = new Object();
    };




    /* 3 Markers */

    //function for determining z-index of the added markers
    function markerOrder(marker, b) {
        //z-index in added order latest on top
        return 1;
    }

    //3.0 Add Marker (object)
    this.AddMarker = function (oMarker) {

        this.AddMarker(Lat, Long, MarkerID, IconKey, OnClick, OnMouseOver, OnMouseOut, MinLat, MaxLat, MinLong, MaxLong, MarkerCount);

    }

    // 3.1 Add Marker
    this.AddMarker = function (nLat, nLong, sMarkerID, sIconKey, oOnClick, oOnMouseOver, oOnMouseOut,
							iMinLat, iMaxLat, iMinLong, iMaxLong, iMarkerCount) {
        if (iMarkerCount == undefined) iMarkerCount = 1;

        //bomb out if not on the earth
        if (Math.abs(nLat) > 90 || Math.abs(nLong) > 180) return;

        //icon
        var oIcon;
        if (sIconKey != undefined) {
            var oCustomIcon = me.Icons[sIconKey];
            oIcon = new GIcon();
            oIcon.image = oCustomIcon.URL;
            oIcon.iconAnchor = new GPoint(oCustomIcon.AnchorX, oCustomIcon.AnchorY);
            oIcon.iconSize = new GSize(oCustomIcon.Width, oCustomIcon.Height);
        } else {
            oIcon = new GIcon(G_DEFAULT_ICON);
        };

        //create and add marker
        var oLatLong = new GLatLng(nLat, nLong);
        var oGMarker = new GMarker(oLatLong, { icon: oIcon, zIndexProcess: markerOrder });
        var oOverlay = me.Map.addOverlay(oGMarker);

        //store marker details
        var oBounds = { MinLat: iMinLat, MaxLat: iMaxLat, MinLong: iMinLong, MaxLong: iMaxLong };
        var oMarker = {
            MarkerID: sMarkerID,
            Marker: oGMarker,
            IconKey: sIconKey,
            ParentMap: me,
            Lat: nLat,
            Long: nLong,
            LatLong: oLatLong,
            Bounds: oBounds,
            ClusterPrefix: null,
            MarkerCount: iMarkerCount
        };
        me.Markers[sMarkerID] = oMarker;

        //add events
        if (oOnClick != undefined) {
            GEvent.addListener(oGMarker, 'click', function () { oOnClick(oMarker); });
        }
        if (oOnMouseOver != undefined) {
            GEvent.addListener(oGMarker, 'mouseover', function () { oOnMouseOver(oMarker); });
        }
        if (oOnMouseOut != undefined) {
            GEvent.addListener(oGMarker, 'mouseout', function () { oOnMouseOut(oMarker); });
        };

        //return the marker
        return oMarker;
    };



    // 3.2 Remove Marker
    this.RemoveMarker = function (oMarker) {
        oMarker = me.SafeMarker(oMarker);
        me.Map.removeOverlay(oMarker.Marker);
        oMarker.ParentMap = null;
        return oMarker;
    };


    // 3.3 Clear Markers
    this.ClearMarkers = function () {
        me.Markers = new Object();
        me.Map.clearOverlays();
    };




    /* 4 Utility Functions */

    // 4.1 Build
    this.Build = function (bCentreAndZoom, sIcons, sEventDefs, sPoints) {

        me.Map.clearOverlays();

        //icons
        me.ClearIcons();
        var aIcons = sIcons.split('#');
        for (var i = 0; i < aIcons.length; i++) {
            var aIcon = aIcons[i].split(',');
            me.AddIcon(aIcon[0], aIcon[1], aIcon[3], aIcon[2], aIcon[4], aIcon[5]); //the reason 2 and 3 are the wrong way round is because the build string is
        };

        //event defs
        var aEventDefs = new Array();
        var aEvents = sEventDefs.split('#');
        for (var i = 0; i < aEvents.length; i++) {
            var aEvent = aEvents[i].split(',');
            aEventDefs[aEvent[0]] = {
                OnClick: function (oMarker) { eval(aEvent[1])(oMarker) },
                OnMouseOver: function (oMarker) { eval(aEvent[2])(oMarker) },
                OnMouseOut: function (oMarker) { eval(aEvent[3])(oMarker) }
            };
        };


        //points
        me.ClearMarkers();
        if (sPoints != '') {
            var aPoints = sPoints.split('#');
            var aPoint;
            for (var i = 0; i < aPoints.length; i++) {
                aPoint = aPoints[i].split(',');
                me.AddMarker(aPoint[1], aPoint[2], aPoint[0], aPoint[4],
						aEventDefs[aPoint[3]].OnClick, aEventDefs[aPoint[3]].OnMouseOver, aEventDefs[aPoint[3]].OnMouseOut,
							aPoint[5], aPoint[6], aPoint[7], aPoint[8]);
            };
        };

        //only zoom and centre if this is the first stab at properties
        if (bCentreAndZoom) {
            me.CentreAndZoom();
        };
        me.ManualZoom = false;
    };


    // 4.2 Remove Outliers
    this.RemoveOutliers = function (nTolerance, iMaxIterations) {
        if (nTolerance == undefined) nTolerance = 3; // 3 standard deviations from the mean should capture 99.73% of samples if they are normally distributed (the three sigma rule)
        if (iMaxIterations == undefined) iMaxIterations = 5; //5 should be plenty, even for the most ridiculous data

        for (var i = 0; i < iMaxIterations; i++) {

            // get the mean location on the map of all the markers
            var aMarkers = new Array();
            for (var marker in me.Markers) {
                oMarker = me.Markers[marker];
                if (oMarker.ParentMap == me) aMarkers.push(oMarker);
            };

            var oMeanCoordinates = me.GetMeanGeographicalCoordinates(aMarkers);
            var oMeanLatLng = new GLatLng(oMeanCoordinates.Lat, oMeanCoordinates.Long);

            // add up all the squared distances from each marker to the mean
            var aDistances = new Array();
            var nSquaredDistances = 0;
            for (var j = 0; j < aMarkers.length; j++) {
                aDistances.push(oMeanLatLng.distanceFrom(aMarkers[j].LatLong));
                nSquaredDistances += aDistances[j] * aDistances[j];
            };

            // get the standard deviation from the mean location
            var nStandardDeviation = Math.sqrt(nSquaredDistances / aMarkers.length);

            // remove any markers that are outside the tolerance
            var bContinue = false;
            for (var j = 0; j < aMarkers.length; j++) {
                if (aDistances[j] > nStandardDeviation * nTolerance) {
                    me.Map.removeOverlay(aMarkers[j].Marker);
                    aMarkers[j].ParentMap = null;
                    bContinue = true;
                };
            };

            if (!bContinue) break;
        };
    };


    // 4.3 Cluster
    this.Cluster = function (sClusteringRadius, sClusterPrefix, oClusteringPredicate, oClusterDefiner, iMinMarkersPerCluster, iMaxMarkersPerCluster) {
        if (iMinMarkersPerCluster == undefined) iMaxMarkersPerCluster = 0;
        if (iMaxMarkersPerCluster == undefined) iMaxMarkersPerCluster = Infinity;

        // find the markers to consider
        var oClusterMarkers = new Array();
        var oMarker;

        for (var marker in me.Markers) {
            oMarker = me.Markers[marker];
            if (oMarker.ParentMap == me && (oMarker.ClusterPrefix == null || oMarker.ClusterPrefix == sClusterPrefix) && oClusteringPredicate(oMarker))
                oClusterMarkers.push(oMarker);
        };

        // create the threshold function first
        var iRadius = parseInt(sClusteringRadius);
        var sMeasurement = sClusteringRadius.substring(iRadius.toString().length);

        var oThresholdFunction;
        switch (sMeasurement) {
            case 'px':
                var oMarker1Position;
                var oMarker2Position;
                var iHorizontalDistance;
                var iVerticalDistance;

                oThresholdFunction = function (oMarker1, oMarker2) {
                    oMarker1Position = me.Map.fromLatLngToDivPixel(oMarker1.LatLong);
                    oMarker2Position = me.Map.fromLatLngToDivPixel(oMarker2.LatLong);
                    iHorizontalDistance = Math.abs(oMarker1Position.x - oMarker2Position.x);
                    iVerticalDistance = Math.abs(oMarker1Position.y - oMarker2Position.y);
                    return Math.sqrt(iHorizontalDistance * iHorizontalDistance + iVerticalDistance * iVerticalDistance) <= iRadius;
                };
                break;
            case 'ml':
                oThresholdFunction = function (oMarker1, oMarker2) {
                    return oMarker1.LatLong.distanceFrom(oMarker2) * 621.371192 <= iRadius;
                };
                break;
            case 'km':
                oThresholdFunction = function (oMarker1, oMarker2) {
                    return oMarker1.LatLong.distanceFrom(oMarker2) * 1000 <= iRadius
                };
                break;
            case 'mt':
                oThresholdFunction = function (oMarker1, oMarker2) {
                    return oMarker1.LatLong.distanceFrom(oMarker2) <= iRadius
                };
                break;
        };

        // now cluster!
        // this is based on a very simple algorithm which I found here: 'http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps'
        var oClusters = new Array();
        var oNonClustered = new Array();
        var oBaseMarker;
        var oClusterMarkerIndexes;
        var oCluster;

        while (oBaseMarker = oClusterMarkers.pop()) {
            oClusterMarkerIndexes = new Array();

            for (var i = 0; i < oClusterMarkers.length; i++) if (oClusterMarkerIndexes.length < iMaxMarkersPerCluster && oThresholdFunction(oBaseMarker, oClusterMarkers[i]))
                oClusterMarkerIndexes.push(i);

            if (oClusterMarkerIndexes.length >= iMinMarkersPerCluster) {
                oCluster = new Array(oBaseMarker);
                for (var i = oClusterMarkerIndexes.length - 1; i >= 0; i--) {
                    oCluster.push(oClusterMarkers[oClusterMarkerIndexes[i]]);
                    oClusterMarkers.splice(oClusterMarkerIndexes[i], 1);
                }
                oClusters.push(oCluster);
            } else {
                oNonClustered.push(oBaseMarker);
            };
        };

        // remove any existing clusters with this prefix
        for (var marker in me.Markers) if (marker.indexOf(sClusterPrefix) == 0 && me.Markers[marker].ParentMap == me) me.RemoveMarker(marker);

        // find the middle of each cluster, add the cluster to the map, and remove the original markers if they are visible
        var oClusterDefinition;
        var oBounds;
        var oCenterLatLong;
        var oSouthWestLatLong;
        var oNorthEastLatLong;
        var oCluster;
        var oMarker;

        for (var i = 0; i < oClusters.length; i++) {
            oCluster = oClusters[i];
            oClusterDefinition = oClusterDefiner(sClusterPrefix + i, oCluster.length);
            oBounds = new GLatLngBounds();

            for (var j = 0; j < oCluster.length; j++) {
                oMarker = oCluster[j];

                oBounds.extend(oMarker.LatLong);
                if (oMarker.ClusterPrefix == null) {
                    oMarker.ClusterPrefix = sClusterPrefix;
                    me.Map.removeOverlay(oMarker.Marker);
                };
            };

            oCenterLatLong = me.GetMeanGeographicalCoordinates(oCluster);
            oSouthWestLatLong = oBounds.getSouthWest();
            oNorthEastLatLong = oBounds.getNorthEast();

            me.AddMarker(oCenterLatLong.Lat, oCenterLatLong.Long, sClusterPrefix + i, oClusterDefinition.IconKey,
                oClusterDefinition.OnClick, oClusterDefinition.OnMouseOver, oClusterDefinition.OnMouseOut,
                oSouthWestLatLong.lat(), oNorthEastLatLong.lat(), oSouthWestLatLong.lng(), oNorthEastLatLong.lng(), oCluster.length);
        };

        // add the nonclustered ones back in if they have been removed previously
        for (var i = 0; i < oNonClustered.length; i++) {
            oMarker = oNonClustered[i];

            if (oMarker.ClusterPrefix == sClusterPrefix) {
                me.Map.addOverlay(oMarker.Marker);
                oMarker.ClusterPrefix = null;
            };
        };
    };




    /* 5 Support Functions */

    // 5.1 Safe Marker
    this.SafeMarker = function (oMarker) {
        if (typeof (oMarker) == 'object') {
            return oMarker;
        } else if (typeof (oMarker) == 'string') {
            return me.Markers[oMarker];
        } else {
            return null;
        };
    };


    // 5.2 Centre And Zoom
    this.CentreAndZoom = function (MaxZoomLevel) {
        if (MaxZoomLevel == undefined) MaxZoomLevel = Infinity;

        me.Map.checkResize();

        var oBounds = new GLatLngBounds();
        var oMarker;

        for (var marker in me.Markers) {
            oMarker = me.Markers[marker];

            if (oMarker.ParentMap == me) {
                oBounds.extend(oMarker.LatLong);
            };
        };
        me.OriginalBounds = oBounds;

        var iZoomLevel = me.Map.getBoundsZoomLevel(oBounds);
        if (MaxZoomLevel < iZoomLevel) iZoomLevel = MaxZoomLevel;

        me.Map.setZoom(iZoomLevel);
        me.Map.setCenter(oBounds.getCenter());
    };


    // 5.3 Zoom To Marker
    this.ZoomToMarker = function (oMarker, MaxZoomLevel) {
        oMarker = me.SafeMarker(oMarker);
        if (MaxZoomLevel == undefined) MaxZoomLevel = Infinity;

        me.Map.checkResize();

        var iZoomLevel;
        var oCenterLatLong;

        if (oMarker.Bounds.MinLat == undefined || oMarker.Bounds.MaxLat == undefined || oMarker.Bounds.MinLong == undefined || oMarker.Bounds.MaxLong == undefined) {
            iZoomLevel = MaxZoomLevel != Infinity ? MaxZoomLevel : me.Map.getZoom();
            oCenterLatLong = oMarker.LatLong;
        } else {
            var oBounds = new GLatLngBounds(new GLatLng(oMarker.Bounds.MinLat, oMarker.Bounds.MinLong), new GLatLng(oMarker.Bounds.MaxLat, oMarker.Bounds.MaxLong));
            iZoomLevel = me.Map.getBoundsZoomLevel(oBounds);
            oCenterLatLong = oBounds.getCenter();
            if (MaxZoomLevel < iZoomLevel) iZoomLevel = MaxZoomLevel;
        };

        me.Map.setZoom(iZoomLevel);
        me.Map.setCenter(oCenterLatLong);
    };


    // 5.4 Bounds
    this.Bounds = function () {
        var oBounds = me.Map.getBounds();
        var oSouthWest = oBounds.getSouthWest();
        var oNorthEast = oBounds.getNorthEast();
        return { StartLat: oSouthWest.lat(), StartLong: oSouthWest.lng(), EndLat: oNorthEast.lat(), EndLong: oNorthEast.lng() };
    };


    // 5.5 Get Marker Coordinates
    this.GetMarkerCoodinates = function (oMarker) {
        oMarker = me.SafeMarker(oMarker);

        var oCoordinates = me.Map.fromLatLngToContainerPixel(oMarker.LatLong);
        return { x: oCoordinates.x, y: oCoordinates.y };
    };


    // 5.6 Get Marker Screen Position
    this.GetMarkerScreenPosition = function (oMarker) {
        var oMapPosition = e.GetPosition(me.MapObject);
        var oMarkerCoords = me.GetMarkerCoodinates(oMarker);
        return { Left: oMapPosition.Left + oMarkerCoords.x, Top: oMapPosition.Top + oMarkerCoords.y };
    };


    // 5.7 Get Mean Geographical Coordinates
    this.GetMeanGeographicalCoordinates = function (aMarkers) {
        var nSumLatitude = 0;
        var nSumLongitude = 0;

        var nLatitude;
        var nLongitude;
        var nLatitudeDifference = 0;
        var nLongitudeDifference = 0;

        for (var i = 0; i < aMarkers.length; i++) {
            nLatitude = aMarkers[i].Lat;
            nLongitude = aMarkers[i].Long;
            if (i != 0) nLatitudeDifference = nLatitude - nSumLatitude / i;
            if (i != 0) nLongitudeDifference = nLongitude - nSumLongitude / i;

            if (nLatitudeDifference > 90) {
                nLatitude -= 180;
            } else if (nLatitudeDifference < -90) {
                nLatitude += 180;
            };

            if (nLongitudeDifference > 180) {
                nLongitude -= 360;
            } else if (nLongitudeDifference < -180) {
                nLongitude += 360;
            };

            nSumLatitude += nLatitude;
            nSumLongitude += nLongitude;
        };

        var nMeanLatitude = nSumLatitude / aMarkers.length;
        var nMeanLogitude = nSumLongitude / aMarkers.length;

        if (nMeanLatitude > 90) {
            nMeanLatitude -= 180;
        } else if (nMeanLatitude < -90) {
            nMeanLatitude += 180;
        };

        if (nMeanLogitude > 180) {
            nMeanLogitude -= 360;
        } else if (nMeanLogitude < -180) {
            nMeanLogitude += 360;
        };

        return { Lat: nMeanLatitude, Long: nMeanLogitude };
    };




};
