var EHBasicGeoDataModel = function (name, latitude, longitude, title) {
    this.Name = name;
    this.Latitude = latitude;
    this.Longitude = longitude;
    this.Title = title;
};
ListAndMapViewData = { 
    BestFitMap: false, //whether to best fit the map to the pins
    Category: null,
    CentrePin: null, //marker for when location search has a centre on the map
    CentrePinTitle: "", //for location search 
    CentreLatLong: null, //lat long for the location search
    ClusterPageSize: 1, // How many items to show on the map popup when there is a cluster of items.  Was 2.
    ClusterShowNumber: false,
    ClusterClicked: false,  //Hack for click event on cluster also firing the google map click event (which we use to close popup)
    CurrentLatitude: 0,
    CurrentLongitude: 0,
    Debugging: true,
    DefaultLatitude: 53.12040, 
    DefaultLongitude: -2.52685,
    DefaultView: "List",
    GeocodeLatLongWhenGetData: false, //If true will get latlng when gets json data.  If false will do filter existing json using clientside ajax call.  I.e Property and event search will already have geocoded the location and return with the results.
    Json: null,
    ListViewPageSize: 10,
    ManualSearchTriggered : false, //Flag to stop url change firing before manual search has finished.
    Map: null,
    MapViewInitialised: false,
    Markers: [],
    // The furthest the user can zoom out is to show the whole of the UK, 
    // this simplified set of numbers covers the UK plus a bit of sea round the edge.
    MaxBounds: null, 
    NearestMileLimit: -1, //will search for everything
    PageTitle: null,
    PageTitlePlural: null,
    PinLayer: null,
    PinIdToZoomTo: -1,
    Results: null,
    Results1: null, //Used if doing two data sets.
    Results1Title: null, //Used if doing two data sets.
    Results2: null, //Used if doing two data sets.
    Results2Title: null, //Used if doing two data sets.
    ShowAllUsed: false,
    SortBy: null,
    Theme: null,
    ToolTipInfoBox: null,
    TwoDatasets: false, // Family days out uses property and events
    UseClustering: true,
    UseFilters: true,
    ZoomLevelInitial: 6,
    ZoomLevelDefaultForCentrePin: 9,
    GetResultCount: function () {
        return this.Results == null ? 0 : this.Results.push ? this.Results.length : -1;
    },
    IsListVisible: function () {
        //List tab is showing
        var listTabContents = $('#list-view');
        if (listTabContents) {
            return listTabContents.is(":visible");
        }
        return false;
    },
    IsListRendered: function () {
        //List tab might show, but has the html rendered?  Can happen if geocoding on page load and it doesn't find a geocode match.
        if (this.IsListVisible) {
            if ($(".property-search-summary .result-count").text().length > 0) {
                return true;
            }
        }
        return false;
    },
    IsMapLoaded: function () {
        return (this.Map && this.Results && this.Results.push);
    },
    IsMapVisible: function () {
        if (this.IsMapLoaded()) { //Map loaded and the map tab is showing
            var mapTabContents = $('#map-view');
            if (mapTabContents) {
                return mapTabContents.is(":visible");
            }
        }
        return false;
    },

    /// <summary>Apply any filters (when applicable) to the search results</summary>
    /// <returns>An array of search results which are applicable to the applied filters</returns>
    Data_ApplyFilters: function () {
        var results = ListAndMapViewData.Results;

        var checkFilters = ListAndMapViewData.Data_CheckFilterItems();
        var searchLocation = ListAndMapViewData.CentreLatLong ? ListAndMapViewData.CentreLatLong : null;
        var filteredResults = [];
        $.each(results, function () {
            if (!checkFilters) {
                this.ValidItem = true;
            }
            else {
                this.ValidItem = ListAndMapViewData.Data_CheckFilters_Extended(this);
            }
            if (this.ValidItem) {
                this.Distance = 0; //reset distance otherwise stuck with previous one if we had no location to search on.
                if (searchLocation) {
                    var latLng = new google.maps.LatLng(this.Latitude, this.Longitude);
                    this.Distance = ListAndMapViewData.Data_GetDistanceBetweenPointsMiles(searchLocation, latLng)
                }
                filteredResults.push(this);
            }
        });
        return filteredResults;
    },

    //Called by ListView_RenderResults to flag if it needs to filter out results from the list results.  
    //This will return true or false depending on if any filters were selected.
    Data_CheckFilterItems: function () {
        if (ListAndMapViewData.UseFilters) {
            return ListAndMapViewData.Data_CheckFilterItemsSelected_Extended();
        }
        else {
            return false;
        }
    },

    //Fired when any of filters on left are changed.
    Data_FilterChanged: function () {
        ListAndMapViewData.MessageBox_Hide();
        ListAndMapViewData.MapPopUp_Close();
        ListAndMapViewData.ListView_SetPageNumber(1);
        ListAndMapViewData.Map_PlotData();
    },

    //Send a string of words or words with whitespace and each first letter is capitalised.
    Data_CapitaliseEachWord: function (str) {
        if (str == null) {
            return "";
        }
        return str.replace(/\w\S*/g, function (txt) {
            return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
        });
    },

    /// <summary>Compares two objects</summary>
    /// <param name="a">Item to compare</param>
    /// <param name="b">Item to compare</param>
    /// <returns>
    /// <para>1: Item "a" is less than item "b" (a < b)</para> 
    /// <para>0: items are equal (a == b)</para>
    /// <para>1: Item "a" is greater than item "b" (a > b)</para>
    /// </returns>
    Data_Compare: function (a, b) {
        a = a !== null ? a : ""; // a parameter of 0 returns false when using "a ?" therefore a distance 
        b = b !== null ? b : ""; // with a value of 0 would be changed to "" making it NaN
        var numericA = parseFloat(a);
        var numericB = parseFloat(b);
        // Compare the item as a string to the item parsed to a number and converted to a string, 
        // if they are the same, the parameters are likely to be numeric so use the numeric version.
        // I prefer this to isNaN as the data may be a string that starts with a number.
        if (a.toString() == numericA.toString() && b.toString() == numericB.toString()) {
            a = numericA;
            b = numericB;
        } else {
            a = a ? a.toLowerCase() : "";
            b = b ? b.toLowerCase() : "";
        }
        if (a < b) { return -1 }
        if (a > b) { return 1 }
        return 0;
    },

    /// Decodes a html encoded string into proper html.
    Data_DecodeHTML: function (value) {
        return $("<textarea/>").html(value).text();
    },

    /// <summary>Gets the distance between two points in miles</summary>
    /// <param name="p1">One of the points in the calculation</param>
    /// <param name="p2">One of the points in the calculation</param>
    Data_GetDistanceBetweenPointsMiles: function (p1, p2) {
        var p1LatRadians = p1.lat() * Math.PI / 180;
        var p1LngRadians = p1.lng() * Math.PI / 180;
        var p2LatRadians = p2.lat() * Math.PI / 180;
        var p2LngRadians = p2.lng() * Math.PI / 180;
        var earthRadius = 3959;
        var d = Math.acos(Math.sin(p1LatRadians) * Math.sin(p2LatRadians) +
            Math.cos(p1LatRadians) * Math.cos(p2LatRadians) *
            Math.cos(p2LngRadians - p1LngRadians)) * earthRadius;
        return isNaN(d) ? 0 : d;
    },

    Data_SortResults: function (results) {
        ListAndMapViewData.Debug_Log("start: Data_SortResults");
        results = results ? results : ListAndMapViewData.Results;
        var searchText = ListAndMapViewData.CentrePinTitle;
        //If we've got a location found for our search. -- only property search does it this way.
        if (ListAndMapViewData.CentrePin) {
            ListAndMapViewData.Debug_Log("start: Data_SortResults - sorting by distance");
            results.sort(ListAndMapViewData.Data_SortByDistance);
            //Limit results to those within *ListAndMapViewData.NearestMileLimit* miles
            if (ListAndMapViewData.NearestMileLimit > 0) {
                for (i = 0; i < results.length; i++) {
                    if (parseFloat(results[i].Distance) > ListAndMapViewData.NearestMileLimit && results[i].Region.toLowerCase() != searchText) {
                        results.splice(i, 1);
                        i--;
                    }
                }
            }
        }
            //No location for our search, so sort on whatever has been chosen.
        else {
            if (ListAndMapViewData.Data_SortResults_Extended) { //If we have a custom sort.
                results = ListAndMapViewData.Data_SortResults_Extended(results);
            }
            else { //No custom sort, so sort by name.
                results = results.sort(ListAndMapViewData.Data_SortByName);
            }
        }
        ListAndMapViewData.Debug_Log("end: Data_SortResults");
        return results;
    },

    /// <summary>Performs an ajax call to get results for displaying</summary>
    /// <param name="retryCount">The number of times this has already been tried</param>
    Data_GetJsonResults: function (retryCount) {
        retryCount = parseInt(retryCount);
        retryCount = isNaN(retryCount) ? 0 : retryCount;
        ListAndMapViewData.Debug_Log("start: Data_GetJsonResults(" + retryCount + ")");
        if (ListAndMapViewData.GetJsonResultsLocked) {
            ListAndMapViewData.Debug_Log("debug: GetJsonResults has been locked");
            return;
        }

        //Add method for checking if we already have our data (i.e map group gets it from the block)
        if (ListAndMapViewData.Json != null) {
            ListAndMapViewData.Results = ListAndMapViewData.Data_GetLocations();
            ListAndMapViewData.Data_GetJsonResultsSuccess();
        }
            //No data yet so go off to relevent API etc to get it.
        else {

            var url = ListAndMapViewData.Url_GetWebApiPath_Extended();

            $.ajax({
                type: "GET",
                url: url,
                contentType: "application/json; charset=utf-8",
                data: "",
                dataType: "json",
                success: function (msg) {
                    ListAndMapViewData.Json = msg;
                    if (ListAndMapViewData.TwoDatasets) {
                        ListAndMapViewData.Results1 = ListAndMapViewData.Data_GetLocations(ListAndMapViewData.Json["Datasets"][0]["Results"]);
                        ListAndMapViewData.Results2 = ListAndMapViewData.Data_GetLocations(ListAndMapViewData.Json["Datasets"][1]["Results"]);
                        //Merge the two results into a single one for the map view.
                        ListAndMapViewData.Results = ListAndMapViewData.Data_GetJoinedLocations(ListAndMapViewData.Results1, ListAndMapViewData.Results2);
                        if (ListAndMapViewData.Json["GeocodedLocation"] != null) {
                            ListAndMapViewData.CurrentLatitude = ListAndMapViewData.Json["GeocodedLocation"].Latitude;
                            ListAndMapViewData.CurrentLongitude = ListAndMapViewData.Json["GeocodedLocation"].Longitude;
                        }
                    }
                    else { //Standard single dataset.
                        ListAndMapViewData.Results = ListAndMapViewData.Data_GetLocations();
                    }

                    //No results try again.
                    if (ListAndMapViewData.Results == null) {
                        if (retryCount < 5 && !ListAndMapViewData.GetJsonResultsLocked) {
                            ListAndMapViewData.Data_GetJsonResults(retryCount + 1);
                        } else {
                            ListAndMapViewData.GetJsonResultsLocked = true;
                        }
                    }
                        //We have results!
                    else {
                        ListAndMapViewData.Data_GetJsonResultsSuccess();
                    }
                },
                error: function (msg) {
                    ListAndMapViewData.Data_GetJsonResultsError(msg);
                    if (retryCount < 5 && !ListAndMapViewData.GetJsonResultsLocked) {
                        ListAndMapViewData.Data_GetJsonResults(retryCount + 1);
                    } else {
                        ListAndMapViewData.GetJsonResultsLocked = true;
                    }
                }
            });
        }
        ListAndMapViewData.Debug_Log("end: Data_GetJsonResults");
    },

    /// <summary>Method to call with the successfully parsed results from Data_GetJsonResults</summary>
    /// <param name="locations">Array of location information for displaying</param>
    Data_GetJsonResultsSuccess: function () {
        ListAndMapViewData.Debug_Log("start: Data_GetJsonResultsSuccess");
        ListAndMapViewData.ListView_DisplayResults(); //Need this if as some listmap views show map first but still want results on list tab.
        if (ListAndMapViewData.DefaultView == "Map") {
            ListAndMapViewData.Map_ResetZoom(); //todo test what happened was commented out
            ListAndMapViewData.Map_ShowMap(); //This will call the plot data.
            //ListAndMapViewData.Map_ResetZoom(); //todo test what happened was commented out
        }
        else if (ListAndMapViewData.MapViewInitialised) { //Update results for the map as well if it's been loaded.
            //else if (ListAndMapViewData.IsMapVisible()) {
            //ListAndMapViewData.Map_ResetZoom(); //todo test what happened was commented out
            ListAndMapViewData.Map_PlotData();
            //ListAndMapViewData.Map_ResetZoom(); //todo if here breaks best fit.
        }
        //If twodatasets i.e family days out then this is the only place it is able to set the centre pin on the search.
        if (ListAndMapViewData.CurrentLatitude > 0 && ListAndMapViewData.TwoDatasets) { 
            ListAndMapViewData.Search_SetSearchLocationAsCentre(ListAndMapViewData.CurrentLatitude, ListAndMapViewData.CurrentLongitude, ListAndMapViewData.CentrePinTitle);
        }
        ListAndMapViewData.Debug_Log("end: Data_GetJsonResultsSuccess");
    },

    /// <summary>Method to call on error from Data_GetJsonResults</suvisiblemmary>
    /// <param name="json">Error details</param>
    Data_GetJsonResultsError: function (json) {
        ListAndMapViewData.Debug_Log("Error: " + JSON.stringify(json));
    },

    //Format locations from the results we were returned from web api to have a unique key.
    Data_GetLocations: function (locations) {
        ListAndMapViewData.Debug_Log("start: Data_GetLocations");

        var data = [];
        var key = 0;
        var places = null;
        if (locations == null) {
            places = ListAndMapViewData.Json["Results"];
        }
        else {
            places = locations;
        }
        if (places == null) {
            return null;
        }
        for (var i = 0; i < places.length; i++) {
            var item = places[i];
            item.key = key;
            data.push(item);
            key++;
        }

        ListAndMapViewData.Debug_Log("end: Data_GetLocations");
        return data;
    },


    //Join locations from 1st and 2nd datasets so the map can show them all.
    Data_GetJoinedLocations: function (locations1, locations2) {
        ListAndMapViewData.Debug_Log("start: Data_GetJoinedLocations");
        //Need to loop through both datasets to create our locations we need for the map.  
        //Will need new key numbers, otherwise loc1 and 2 could have same keys so map will get confused.
        var key = 0;
        var data = [];

        //Add locations1 to our data array.
        for (var i = 0; i < locations1.length; i++) {
            var item = locations1[i];
            item.key = key;
            data.push(item);
            key++;
        }

        //Now add any locations2 to that list
        for (var i = 0; i < locations2.length; i++) {
            var item = locations2[i];
            item.key = key;
            data.push(item);
            key++;
        }

        ListAndMapViewData.Debug_Log("end: Data_GetJoinedLocations");
        return data;
    },

    /// <summary>Sorts two objects by their "Title" parameter</summary>
    /// <param name="a">One of the items, with a .Title attribute, to be used in the calculation</param>
    /// <param name="b">One of the items, with a .Title attribute, to be used in the calculation</param>
    /// <returns>
    /// <para>1: Item "a" is less than item "b" (a < b)</para> 
    /// <para>0: items are equal (a == b)</para>
    /// <para>1: Item "a" is greater than item "b" (a > b)</para>
    /// </returns>
    Data_SortByName: function (a, b) {
        return ListAndMapViewData.Data_Compare(a.Title, b.Title);
    },

    /// <summary>Sorts two objects by their "distance" parameter</summary>
    /// <param name="a">One of the items, with a .Distance attribute, to be used in the calculation</param>
    /// <param name="b">One of the items, with a .Distance attribute, to be used in the calculation</param>
    /// <returns>
    /// <para>1: Item "a" is less than item "b" (a < b)</para> 
    /// <para>0: items are equal (a == b)</para>
    /// <para>1: Item "a" is greater than item "b" (a > b)</para>
    /// </returns>
    Data_SortByDistance: function (a, b) {
        return ListAndMapViewData.Data_Compare(a.Distance, b.Distance);
    },

    /// <summary>Sorts two objects by their "id" parameter</summary>
    /// <param name="a">One of the items, with a .ID attribute, to be used in the calculation</param>
    /// <param name="b">One of the items, with a .ID attribute, to be used in the calculation</param>
    /// <returns>
    /// <para>1: Item "a" is less than item "b" (a < b)</para> 
    /// <para>0: items are equal (a == b)</para>
    /// <para>1: Item "a" is greater than item "b" (a > b)</para>
    /// </returns>
    Data_SortById: function (a, b) {
        return ListAndMapViewData.Data_Compare(a.ID, b.ID);
    },

    /// <summary>Sorts two objects by their "Key" parameter</summary>
    /// <param name="a">One of the items, with a .Key attribute, to be used in the calculation</param>
    /// <param name="b">One of the items, with a .Key attribute, to be used in the calculation</param>
    /// <returns>
    /// <para>1: Item "a" is less than item "b" (a < b)</para> 
    /// <para>0: items are equal (a == b)</para>
    /// <para>1: Item "a" is greater than item "b" (a > b)</para>
    /// </returns>
    Data_SortByKey: function (a, b) {
        return ListAndMapViewData.Data_Compare(a.Key, b.Key);
    },

    /// <summary>When gDebug_Log is true, logs the provided message to the console</summary>
    /// <param name="txt">Message to log</param>
    Debug_Log: function (txt) {
        if (ListAndMapViewData.Debugging) {
            console.log(txt);
        }
    },

    Initialise: function (category, theme, defaultView, debug, pageTitle, pageTitlePlural, defaultLat, defaultLng, useFilters, pageSize, enableClustering, largePopupImages, bestFit) {
        ListAndMapViewData.Debug_Log("start: ListAndMapViewInitialLoad_Callback");
        ListAndMapViewData.InitialiseElements();
        ListAndMapViewData.MessageBox_Hide();
        ListAndMapViewData.MapPopUp_Close();


        ListAndMapViewData.MaxBounds = new google.maps.LatLngBounds(
            new google.maps.LatLng(49.5, -11),
            new google.maps.LatLng(60, 2.5)
        );
          

        ListAndMapViewData.DefaultView = defaultView;
        ListAndMapViewData.PageTitle = pageTitle;
        ListAndMapViewData.PageTitlePlural = pageTitlePlural;
        ListAndMapViewData.Category = category;
        ListAndMapViewData.ListViewPageSize = pageSize;
        ListAndMapViewData.Theme = theme;
        ListAndMapViewData.UseClustering = enableClustering;
        ListAndMapViewData.LargePopupImages = largePopupImages;
        ListAndMapViewData.BestFitMap = bestFit;

        //If need different centre point for map (i.e holiday cottages moves down to Birmingham)
        if (defaultLat != 0) {
            ListAndMapViewData.DefaultLatitude = defaultLat;
        }
        if (defaultLng != 0) {
            ListAndMapViewData.DefaultLongitude = defaultLng;
        }
        ListAndMapViewData.DefaultCentre = new google.maps.LatLng(defaultLat, defaultLng)
        ListAndMapViewData.UseFilters = useFilters;

        ListAndMapViewData.SearchParams_Load();

        ListAndMapViewData.Data_GetJsonResults();

        /*if (ListAndMapViewData.DefaultView == "Map") {
            ListAndMapViewData.Map_ShowMap();
        }*/

        ListAndMapViewData.Debug_Log("end: ListAndMapViewInitialLoad_Callback");
    },

    InitialiseElements: function () {
        $('#listView-prevPage')
            .click(function () {
                var currentPage = ListAndMapViewData.ListView_GetPageNumber();
                if (!currentPage) {
                    currentPage = 1;
                } else {
                    currentPage--;
                }
                ListAndMapViewData.ListView_SetPageNumber(currentPage);
                ListAndMapViewData.ListView_ScrollToFirstResult();
                return false;
            });
        $('#listView-nextPage')
            .click(function () {
                var lastPage = Math.ceil(parseInt($('.result-count').text()) / ListAndMapViewData.ListViewPageSize);
                var currentPage = ListAndMapViewData.ListView_GetPageNumber();
                if (currentPage < lastPage) {
                    currentPage++;
                    ListAndMapViewData.ListView_SetPageNumber(currentPage);
                }
                $('#listView-prevPage').show();
                ListAndMapViewData.ListView_ScrollToFirstResult();
                return false;
            });
        $('#listView-showAll')
            .click(function () {
                ListAndMapViewData.ListView_SetPageNumber(0);
                //Hide show all after clicking as can't show all again (unless went a show less option).
                $('#listView-showAll').hide();
                //ListAndMapViewData.ListView_ScrollToFirstResult();
                ListAndMapViewData.ShowAllUsed = true;
                return false;
            });
        if (ListAndMapViewData.TwoDatasets) {
            $('#listView1-showMore').click(function () {
                var lastPage = Math.ceil(parseInt($('#summary-result-count1').text()) / ListAndMapViewData.ListViewPageSize);
                var currentPage = ListAndMapViewData.ListView_GetPageNumber(1);
                if (currentPage < lastPage) {
                    currentPage++;
                    ListAndMapViewData.ListView_SetPageNumber(currentPage, ListAndMapViewData.ListView_GetPageNumber(2));
                }
                return false;
            });
            $('#listView2-showMore').click(function () {
                var lastPage = Math.ceil(parseInt($('#summary-result-count2').text()) / ListAndMapViewData.ListViewPageSize);
                var currentPage = ListAndMapViewData.ListView_GetPageNumber(2);
                if (currentPage < lastPage) {
                    currentPage++;
                    ListAndMapViewData.ListView_SetPageNumber(ListAndMapViewData.ListView_GetPageNumber(1), currentPage);
                }
                return false;
            });
        }

        //The main search button.
        $("#locationSearchButton").click(function (e) {
            ListAndMapViewData.Map_ClearCentrePin();
            e.preventDefault();
            if ($('#locationSearchField').val() != "") {
                //Clear any my location search data.
                ListAndMapViewData.CurrentLatitude = 0;
                ListAndMapViewData.CurrentLongitude = 0;
                ListAndMapViewData.CentrePinTitle = $('#locationSearchField').val();
                //Do search.
                ListAndMapViewData.Search_RunSearch();
            }
            else {
                ListAndMapViewData.Search_Reset();
            }
            //ListAndMapViewData.ListView_SetPageNumber(1); todo this is casuing issues
        });

        //The search criteria input box itself.
        $("#locationSearchField").keypress(function (e) {
            if (e.which == 13) {
                e.preventDefault();
                if ($('#locationSearchField').val() != "") {
                    //Clear any my location search data.
                    ListAndMapViewData.CurrentLatitude = 0;
                    ListAndMapViewData.CurrentLongitude = 0;
                    ListAndMapViewData.CentrePinTitle = $('#locationSearchField').val();
                    //Do search.
                    ListAndMapViewData.Search_RunSearch();
                }
                else {
                    ListAndMapViewData.Search_Reset();
                }
                //ListAndMapViewData.ListView_SetPageNumber(1); todo this is casuing issues
            }
        });

        //Load custom elements.
        if (ListAndMapViewData.InitialiseElements_Extended) {
            ListAndMapViewData.InitialiseElements_Extended();
        }

        //This will trigger every time a user tries to do a search, click next/prev or go back/forward in browser.
        window.onhashchange = ListAndMapViewData.Url_LocationHashChanged;
    },

    ///<summary>Scroll to the first result in the list view</summary>
    ListView_ScrollToFirstResult: function () {
        var scrollToY = $('#result-count-parent').offset().top;
        window.scrollTo(0, scrollToY);
    },

    ///<summary>Scroll to the top of the list view (where the result count is displayed)</summary>
    ListView_ScrollToResultCount: function () {
        var scrollToY = $('#result-count-parent').offset().top;
        window.scrollTo(0, scrollToY - 60);
    },


    //Set the number of the page being displayed in the List View
    ///<param name="pageNumber">Page number to be displayed</param>
    ListView_SetPageNumber: function (pageNumberDS1, pageNumberDS2) {
        ListAndMapViewData.Debug_Log("start: ListView_SetPageNumber");
        //Only set hash if we have this method set, otherwise no action needed.
        if (ListAndMapViewData.Url_GetQueryString_Extended) {
            if (pageNumberDS2 == null) { //No second set of page numbers so standard single dataset.
                pageNumber = parseInt(pageNumberDS1);
                if (isNaN(pageNumber)) {
                    pageNumber = 1;
                }
                ListAndMapViewData.Url_SetLocationHash(pageNumber);
            }
            else { //Both numbers have a value so we are doing two data sets.
                pageNumberDS1 = parseInt(pageNumberDS1);
                if (isNaN(pageNumberDS1)) {
                    pageNumberDS1 = 1;
                }
                pageNumberDS2 = parseInt(pageNumberDS2);
                if (isNaN(pageNumberDS2)) {
                    pageNumberDS2 = 1;
                }
                ListAndMapViewData.Url_SetLocationHash(pageNumberDS1, pageNumberDS2);
            }
        }
        else {
            ListAndMapViewData.Debug_Log("No Url_GetQueryString_Extended method so no action required");
        }
        ListAndMapViewData.Debug_Log("end: ListView_SetPageNumber");
    },

    //Updates the UI to display the results in a list view.
    ListView_DisplayResults: function () {
        ListAndMapViewData.Debug_Log("start: ListView_DisplayResults");
        var searchHasRun = false;
        if (!ListAndMapViewData.GeocodeLatLongWhenGetData && ListAndMapViewData.CentrePinTitle.length > 0) {
            searchHasRun = true;
            ListAndMapViewData.Search_RunSearch();
        }

        if (!searchHasRun) {
            //Trigger search if we were passed the lat long
            if (!ListAndMapViewData.GeocodeLatLongWhenGetData && ListAndMapViewData.CurrentLatitude != 0 && ListAndMapViewData.CurrentLongitude != 0) {
                searchHasRun = true;
                ListAndMapViewData.Search_RunSearch();
            }
        }

        //No filter search or manual search was needed, so just show results and need to trigger the querystring update and ListView_RenderResults.
        if (!searchHasRun) {
            //This will trigger the url change and fire ListView_RenderResults().
            if (!ListAndMapViewData.TwoDatasets) {
                ListAndMapViewData.ListView_SetPageNumber(1);
            }
            else { //Two datasets.
                ListAndMapViewData.ListView_SetPageNumber(1, 1);
            }
        }
        ListAndMapViewData.Debug_Log("end: ListView_DisplayResults");
    },

    //Get the number of the page being displayed in the List View
    ListView_GetPageNumber: function (datasetNo) {
        //Standard.
        if (datasetNo == null) {
            var parsedPageNumber = parseInt(ListAndMapViewData.Url_GetParameterByName("page"));
            parsedPageNumber = (isNaN(parsedPageNumber) ? 1 : parsedPageNumber);
            return parsedPageNumber;
        }
            //Two page numbers stored in querystring.
        else {
            var parsedPageNumber = 1;
            if (datasetNo == 1) {
                parsedPageNumber = parseInt(ListAndMapViewData.Url_GetParameterByName("pageDS1"));
            }
            else //Dataset2
            {
                parsedPageNumber = parseInt(ListAndMapViewData.Url_GetParameterByName("pageDS2"));
            }
            parsedPageNumber = (isNaN(parsedPageNumber) ? 1 : parsedPageNumber);

            return parsedPageNumber;
        }
    },

    //Reset the List View pager
    ListView_ResetPager: function () {
        ListAndMapViewData.Debug_Log("start: ListView_ResetPager");
        //Standard data has only one page querystring.
        if (!ListAndMapViewData.TwoDatasets) {
            var lastPage = Math.ceil(parseInt($('.result-count').text()) / ListAndMapViewData.ListViewPageSize);
            var currentPage = ListAndMapViewData.ListView_GetPageNumber();
            if (currentPage <= lastPage) {
                if (currentPage == lastPage) {
                    $('#listView-nextPage').hide();
                } else {
                    $('#listView-nextPage').show();
                }
            }
            if (currentPage <= 1) {
                $('#listView-prevPage').hide();
                //If back on page 1 and user has clicked show all button already then need to show it again.
                if (currentPage == 1 && ListAndMapViewData.ShowAllUsed) {
                    $('#listView-showAll').show();
                }
            }
        }
            //Two datasets have two page querystrings.
        else {
            var pageNoDS1 = ListAndMapViewData.ListView_GetPageNumber(1);
            var pageNoDS2 = ListAndMapViewData.ListView_GetPageNumber(2);

            var lastPageDataset1 = Math.ceil(ListAndMapViewData.Results1.length / ListAndMapViewData.ListViewPageSize);
            var currentPageDataset1 = pageNoDS1
            if (currentPageDataset1 < lastPageDataset1) {
                var numberForShowMoreButton1 = ListAndMapViewData.ListViewPageSize;
                //Work out how many we are actually showing.
                var currentNumberShownDataset1 = pageNoDS1 * ListAndMapViewData.ListViewPageSize
                //If amount left to show is less than our pagesize then use that number instead.
                //I.e 3 a page, 16 in total we are showing 15 out of 16.  Would want button to say show remaining events.
                if (ListAndMapViewData.Results1.length - currentNumberShownDataset1 < ListAndMapViewData.ListViewPageSize) {
                    numberForShowMoreButton1 = ListAndMapViewData.Results1.length - currentNumberShownDataset1;
                }
                $('#listView1-showMore').text("Show " + numberForShowMoreButton1 + " more " + ListAndMapViewData.Results1Title).append('<span></span>');
                $('#listView1-showMore').show();
                $('#result-count1-current').text(currentNumberShownDataset1);
            }
            else //Last page.
            {
                $('#result-count1-current').text(ListAndMapViewData.Results1.length);
                $('#listView1-showMore').hide();
            }

            var lastPageDataset2 = Math.ceil(parseInt($('#summary-result-count2').text()) / ListAndMapViewData.ListViewPageSize);
            var currentPageDataset2 = pageNoDS2
            if (currentPageDataset2 < lastPageDataset2) {
                var numberForShowMoreButton2 = ListAndMapViewData.ListViewPageSize;
                //Work out how many we are actually showing.
                var currentNumberShownDataset2 = pageNoDS2 * ListAndMapViewData.ListViewPageSize
                //If amount left to show is less than our pagesize then use that number instead.
                //I.e 3 a page, 16 in total we are showing 15 out of 16.  Would want button to say show remaining events.
                if (ListAndMapViewData.Results2.length - currentNumberShownDataset2 < ListAndMapViewData.ListViewPageSize) {
                    numberForShowMoreButton2 = ListAndMapViewData.Results2.length - currentNumberShownDataset2;
                }
                $('#listView2-showMore').text("Show " + numberForShowMoreButton2 + " more " + ListAndMapViewData.Results2Title).append('<span></span>');
                $('#listView2-showMore').show();
                $('#result-count2-current').text(currentNumberShownDataset2);
            }
            else //Last page.
            {
                $('#result-count2-current').text(ListAndMapViewData.Results2.length);
                $('#listView2-showMore').hide();
            }
        }
        ListAndMapViewData.Debug_Log("end: ListView_ResetPager");
    },

    //Filter, sort and then create the results count header and html text results.
    ListView_RenderResults: function () {
        ListAndMapViewData.Debug_Log("start: ListView_RenderResults");


        if (ListAndMapViewData.Results == null) {
            ListAndMapViewData.Debug_Log("end: ListView_RenderResults - No results to render");
            return;
        }

        //Standard data
        if (!ListAndMapViewData.TwoDatasets) {
            var results = ListAndMapViewData.Data_ApplyFilters();
            results = ListAndMapViewData.Data_SortResults(results);
            $('.result-count').text(results.length);

            ListAndMapViewData.ListView_ResetPager();
            var pageNumber = ListAndMapViewData.ListView_GetPageNumber();

            var startPos = (ListAndMapViewData.ListViewPageSize * (pageNumber - 1)) + 1;
            var endPos = startPos + (ListAndMapViewData.ListViewPageSize - 1);

            if (endPos > results.length) {
                endPos = results.length;
            }

            //If 0 user wants all records.
            if (pageNumber == 0) {
                startPos = 1;
                endPos = results.length;
            }

            //Create the html list view from the results, this is custom for each search type.
            if (ListAndMapViewData.ListView_RenderResults_Extended) {
                ListAndMapViewData.ListView_RenderResults_Extended(results, startPos, endPos);
            }
        }
            //Two datasets, so will need to render two lots of html and have two paging options.
        else {
            $('#summary-result-count1').text(ListAndMapViewData.Results1.length);
            $('#summary-result-count2').text(ListAndMapViewData.Results2.length);

            $('#result-count1-total').text(ListAndMapViewData.Results1.length);
            $('#result-count2-total').text(ListAndMapViewData.Results2.length);

            ListAndMapViewData.ListView_ResetPager();
            var pageNumberDS1 = ListAndMapViewData.ListView_GetPageNumber(1);
            var pageNumberDS2 = ListAndMapViewData.ListView_GetPageNumber(2);

            var startPos1 = 1;
            var endPos1 = pageNumberDS1 * ListAndMapViewData.ListViewPageSize;

            if (endPos1 > ListAndMapViewData.Results1.length) {
                endPos1 = ListAndMapViewData.Results1.length;
            }

            var startPos2 = 1;
            var endPos2 = pageNumberDS2 * ListAndMapViewData.ListViewPageSize;

            if (endPos2 > ListAndMapViewData.Results2.length) {
                endPos2 = ListAndMapViewData.Results2.length;
            }
            //Create the html list view from the results, this is custom for each search type.
            if (ListAndMapViewData.ListView_RenderResults_Extended) {
                ListAndMapViewData.ListView_RenderResults_Extended(ListAndMapViewData.Results1, ListAndMapViewData.Results2, startPos1, endPos1, startPos2, endPos2);
            }
        }

        //Force the data equalizer to even up the result grid heights.
        try {
            $(document).foundation('equalizer', 'reflow');
        }
        catch (err) {
            console.log("Foundation not available yet to do equalizer reflow");
        }

        ListAndMapViewData.Debug_Log("end: ListView_RenderResults");
    },

    //Converts the provided locations into a collection of pushpins
    Map_GetPushpinsForLocations: function (locations) {
        var pushpins = [];
        if (locations && locations.push) {
            $.each(locations, function (idx) {
                pushpins.push(ListAndMapViewData.Map_CreateSinglePin(this));
            });
        }
        return pushpins;
    },

    //Enable zoom - intended to be triggered by an event handler called when the map is clicked.  
    //Stop users scrolling down page with mousewheel accidently zooming the map.
    Map_EnableZoom: function (e) {
        ListAndMapViewData.Map.setOptions({ disableZooming: false });
    },

    //Plots the location data onto the map, using all locations i fmap has loaded
    //Will also render the html results.
    Map_PlotData: function () {
        ListAndMapViewData.Debug_Log("start: Map_PlotData");
        if (ListAndMapViewData.MapViewInitialised) {
        //if (ListAndMapViewData.IsMapVisible()) { //TODO had to comment this out as need to be able to update map on manual search when in list view.
            var results = ListAndMapViewData.Data_ApplyFilters();
            if (results && results.push) {
                
                //Clear existing markers and clusters.
                if (ListAndMapViewData.Markers && ListAndMapViewData.Markers.push) {
                    if (ListAndMapViewData.Markers.length > 0) {
                        ListAndMapViewData.Debug_Log("debug: googleMaps-v3.ListAndMapViewData.Map_PlotData\nRemoving " + ListAndMapViewData.Markers.length + " pins");
                        for (var i = 0; i < ListAndMapViewData.Markers.length; i++) {
                            ListAndMapViewData.Markers[i].setMap(null);
                        }
                    }
                    if (ListAndMapViewData.MarkerCluster) {
                        ListAndMapViewData.MarkerCluster.clearMarkers();
                    }
                }
                
                ListAndMapViewData.Debug_Log("Number of items to plot on the map: " + results.length);
                ListAndMapViewData.Markers = ListAndMapViewData.Map_GetPushpinsForLocations(results);

                if (ListAndMapViewData.UseClustering) {
                    var iconUrl = "/static/icons/pin-multiple-clear.png";
                    //Get our custom pin icon.
                    if (ListAndMapViewData.Map_GetMultiplePinIcon_Extended) {
                        iconUrl = ListAndMapViewData.Map_GetMultiplePinIcon_Extended();
                    }

                    //The styles are for small, medium and large clusters.  Normally google maps shows hte number on the icon.
                    var clusterStyles = [
                        {
                            //anchor: [-5,-5], //y,x  -- negative values do not work so can't easily do that here to adjust label position.
                            textColor: 'white',
                            textSize: 14, 

                            //textSize: 0.001,
                            url: iconUrl,
                            //height: 42,
                            //width: 32,
                            height: 40,
                            width: 30
                        }/*,
                        {
                            textColor: 'white',
                            textSize: 0.001, 
                            url: iconUrl,
                            height: 40,
                            width: 30
                        },
                        {
                            textColor: 'white',
                            textSize: 0.001, 
                            url: iconUrl,
                            height: 40,
                            width: 30
                        }*/
                    ];
                    ListAndMapViewData.MarkerCluster = new MarkerClusterer(ListAndMapViewData.Map, ListAndMapViewData.Markers,
                        {                            
                            gridSize: 30,
                            maxZoom: 19,
                            styles: clusterStyles,
                            zoomOnClick: false
                        });

                    /**
                    * Set our own custom marker cluster calculator
                    * It's important to remember that this function runs for EACH
                    * cluster individually.
                    * @param  {Array} markers Set of markers for this cluster.
                    * @param {Number} numStyles Number of styles we have to play with (set in mcOptions).
                    //Note I've set it to only have 1 style, but leave here in case we did want bigger clusters depending on count.
                    */
                    ListAndMapViewData.MarkerCluster.setCalculator(function (markers, numStyles) {
                        //create an index for icon styles
                        var index = 0,
                        //Count the total number of markers in this cluster
                        count = markers.length,
                        //Set total to loop through (starts at total number)
                        total = count;

                        /* While we still have markers, divide by a set number and increase the index. Cluster moves up to a new style.                         
                           The bigger the index, the more markers the cluster contains, so the bigger the cluster. */
                        while (total !== 0) {
                            //Create a new total by dividing by a set number
                            total = parseInt(total / 5, 10);
                            //Increase the index and move up to the next style
                            index++;
                        }

                        /* Make sure we always return a valid index. E.g. If we only have 5 styles, but the index is 8, 
                        this will make sure we return 5. Returning an index of 8 wouldn't have a marker style.  */
                        index = Math.min(index, numStyles);

                        //Tell MarkerCluster this clusters details (and how to style it)
                        return {
                            //text: count + " (" + index + ")",
                            text: "<div class='cluster-text'>" + count + "</div>",
                            index: index
                        };
                    });


                    google.maps.event.addListener(ListAndMapViewData.MarkerCluster, 'clusterclick', function (cluster) {
                        ListAndMapViewData.ClusterClicked = true;
                        ListAndMapViewData.MapPopUp_Open(cluster);
                    });


                }

                //If set to best fit:  - gets called by reset zoom which fires after this one anyway. TODO check this now works better here.
                if (ListAndMapViewData.BestFitMap) {
                    //Set the initial boundary and zoom to emcompass all our pins.
                    var bounds = new google.maps.LatLngBounds();
                    for (var i = 0; i < ListAndMapViewData.Markers.length; i++) {
                        bounds.extend(ListAndMapViewData.Markers[i].getPosition());
                    }

                    //On twodatasets it doesn't cluster quite as well on the best fit mode when searching.
                    if (ListAndMapViewData.UseClustering && ListAndMapViewData.MarkerCluster) {
                        setTimeout(function () {
                            ListAndMapViewData.Map.fitBounds(bounds);
                        }, 300);
                    }
                    else {
                        ListAndMapViewData.Map.fitBounds(bounds);
                    }
                }
            }
            else {
                ListAndMapViewData.Debug_Log("end: Map_PlotData - No data provided to plot");
                return;
            }
        }

        
        //ListAndMapViewData.ListView_RenderResults(); //Check this is called just the once.
        ListAndMapViewData.Debug_Log("end: Map_PlotData");
    },

    Map_ShowMap: function () {
        ListAndMapViewData.Debug_Log("start: Map_ShowMap");
        if (!ListAndMapViewData.IsMapLoaded()) {
            ListAndMapViewData.Map_CreateMap();
        }
        //If map not loaded to page yet.
        if (!ListAndMapViewData.MapViewInitialised) {

            $('.property-map-results-panel').find('.close').click(function () { ListAndMapViewData.MapPopUp_Close(); return false; });
            $('.property-map-results-panel').find('.next').click(function () { ListAndMapViewData.MapPopUp_NextPage(); return false; });
            $('.property-map-results-panel').find('.prev').click(function () { ListAndMapViewData.MapPopUp_PreviousPage(); return false; });
            ListAndMapViewData.MapViewInitialised = true;
            ListAndMapViewData.Map_PlotData(); //was commented out 
            ListAndMapViewData.Map_ZoomToCentrePin();
        }
        //ListAndMapViewData.Map_PlotData(); //having this here means it will keep flickering when clicking the map tab.
        
        ListAndMapViewData.Debug_Log("end: Map_ShowMap");
    },

    Map_ViewChanged: function (args) {
        var zoom = ListAndMapViewData.Map.getZoom();
        if (zoom >= 15 && ListAndMapViewData.MapTypeAutoChanged != true && ListAndMapViewData.Map.getMapTypeId() == Microsoft.Maps.MapTypeId.road) {
            ListAndMapViewData.Map.setView({ mapTypeId: Microsoft.Maps.MapTypeId.aerial });
            ListAndMapViewData.MapTypeAutoChanged = true;
        } else if (zoom < 15 && ListAndMapViewData.MapTypeAutoChanged == true && ListAndMapViewData.Map.getMapTypeId() == Microsoft.Maps.MapTypeId.aerial) {
            ListAndMapViewData.Map.setView({ mapTypeId: Microsoft.Maps.MapTypeId.road });
            ListAndMapViewData.MapTypeAutoChanged = false;
        }
    },

    //Zooms the map view to the location of the centre pin
    Map_ZoomToCentrePin: function () {
        ListAndMapViewData.Debug_Log("start: Map_ZoomToCentrePin");
        ListAndMapViewData.MessageBox_Hide();
        ListAndMapViewData.MapPopUp_Close();
        //Only zoom to the centre marker if we have loaded the map and the marker has been set up.
        if (ListAndMapViewData.IsMapLoaded() && ListAndMapViewData.CentrePin) {
            ListAndMapViewData.CentrePin.setMap(ListAndMapViewData.Map) //Add marker to map.
            if (!ListAndMapViewData.BestFitMap) { //If not best fit then centre on the marker and set the zoom.
                ListAndMapViewData.Map.setCenter(ListAndMapViewData.CentreLatLong); //Center on marker.
                ListAndMapViewData.Map.setZoom(ListAndMapViewData.ZoomLevelDefaultForCentrePin); //Set zoom level.
            }
        } else {
            ListAndMapViewData.Debug_Log("debug: Map_ZoomToCentrePin no changes made to map view.");
        }
        ListAndMapViewData.Debug_Log("end: Map_ZoomToCentrePin");
    },

    //Set the centre marker, but do not necessarily add to the map yet as the map may not be loaded.
    Search_SetSearchLocationAsCentre: function (lat, lng, name) {
        ListAndMapViewData.Debug_Log("start: Search_SetSearchLocationAsCentre");
        ListAndMapViewData.MessageBox_Hide(); 
        ListAndMapViewData.CentreLatLong = new google.maps.LatLng(lat, lng);
        ListAndMapViewData.CentrePin = new google.maps.Marker({
            position: ListAndMapViewData.CentreLatLong,
            //map: ListAndMapViewData.Map,
            map: null,
            title: name,
            icon: {
                url: "/static/icons/places-icon-large.png"
                /*url: "/static/images/eh_pin_locator.png",
                anchor: new google.maps.Point(17,18) //image size is 35,35*/
            },
            zIndex: -1000
        });

        //Only add plot map and add centre if map has been initialised.
        if (ListAndMapViewData.MapViewInitialised) {
            //Testing idea of just zooming here and not plotting.
            //ListAndMapViewData.Map_ZoomToCentrePin();

            //This below does work for property search at least TODO check others.
            //ListAndMapViewData.Map_PlotData();
            //Cluster doesn't always set correctly and can end up grouping items when zoomed in.  Adding slight delay gives it a chance to catch up.
            if (ListAndMapViewData.UseClustering && ListAndMapViewData.MarkerCluster) {
                setTimeout(function () {
                    ListAndMapViewData.Map_ZoomToCentrePin();
                }, 500);
            }
            else {
                ListAndMapViewData.Map_ZoomToCentrePin();
            }
            
        }
        //ListAndMapViewData.ListView_RenderResults(); //Need here so can update list view for the search result.

        ListAndMapViewData.Debug_Log("end: Search_SetSearchLocationAsCentre");
    },

    //Set the centre marker and render the results with the new centre. Used for popup on geocoding when user needs to pick which result they wanted.
    Search_SetSearchLocationAsCentreAndRenderResults: function (lat, lng, name) {
        ListAndMapViewData.Debug_Log("start: Search_SetSearchLocationAsCentreAndRenderResults");
        ListAndMapViewData.Search_SetSearchLocationAsCentre(lat, lng, name);
        ListAndMapViewData.ListView_SetPageNumber(1);
        //ListAndMapViewData.ListView_RenderResults();
        ListAndMapViewData.Debug_Log("end: Search_SetSearchLocationAsCentreAndRenderResults");
    },

    Map_ClearCentrePin: function () {
        //Clear from map
        if (ListAndMapViewData.CentrePin) {
            ListAndMapViewData.CentrePin.setMap(null);
        }
        //Then clear items.
        ListAndMapViewData.CentrePin = null;
        ListAndMapViewData.CentreLatLong = null;
    },

    //Handler triggered when a pin/cluster pin is pressed: opens the pop-up menu
    MapPopUp_Open: function (e) {
        ListAndMapViewData.Debug_Log("start: MapPopUp_Open");
        if (this.Item) {
            ListAndMapViewData.MapPopUp_OpenForPin(this); 
        }
        else if (e.markers_) { //clusters
            ListAndMapViewData.MapPopUp_OpenForCluster(e.markers_);
            ListAndMapViewData.MapPopUp_SetPage(1);
        }
        ListAndMapViewData.Debug_Log("end: MapPopUp_Open");
    },

    ///Open the Map Pop-Up Menu and display data for the provided pushpins
    MapPopUp_OpenForCluster: function (pushpins) {
        ListAndMapViewData.Debug_Log("start: MapPopUp_OpenForCluster");
        ListAndMapViewData.MapPopUp_ClearResults();
        $('#map-view').find(".property-map-results-panel").show();
        //Set the default values for the Page Number / Page Count on the pop-up
        $('#pagination-location-pageNumber').text(1);
        $('#pagination-location-pageCount').text(1);
        $(".property-map-results-summary .pageCount").text(pushpins.length + ' ' + ListAndMapViewData.PageTitlePlural);

        if (pushpins && pushpins.push) {
            $.each(pushpins, function (index) {
                ListAndMapViewData.MapPopUp_AddResult_Extended(this.Item);
            });
        }
        ListAndMapViewData.MapPopUp_SetPage(1);
        ListAndMapViewData.Debug_Log("end: MapPopUp_OpenForCluster");
    },

    ///Open the Map Pop-Up Menu for the provided pin
    MapPopUp_OpenForPin: function (pin) {
        ListAndMapViewData.MessageBox_Hide();
        ListAndMapViewData.Debug_Log("start: MapPopUp_OpenForPin");
        $('#map-view').find(".property-map-results-panel").show();

        ListAndMapViewData.MapPopUp_ClearResults();
        //Set the default values for the Page Number / Page Count on the pop-up
        $('#pagination-location-pageNumber').text(1);
        $('#pagination-location-pageCount').text(1);
        //Single pin
        if (pin.Item != null) {
            ListAndMapViewData.MapPopUp_AddResult_Extended(pin.Item);
            $(".property-map-results-panel .property-map-results-summary").hide();
            $(".property-map-results-panel .pagination").hide();
        }
        ListAndMapViewData.MapPopUp_SetPage(1);

        if (ListAndMapViewData.MapPopUp_OpenForPin_Extended) {
            ListAndMapViewData.MapPopUp_OpenForPin_Extended(); ////As map group is a larger map we need to ensure when clicking popup you go to the middle of the map.
        }

        ListAndMapViewData.Debug_Log("end: MapPopUp_OpenForPin");
    },

    //Clears the properties currently displayed on the Map Pop-Up Menu
    MapPopUp_ClearResults: function () {
        $('#map-view').find(".property-search-results").empty();
    },

    //Navigate to the Next page of results on the Map Pop-Up Menu (displays collection of properties at clicked pin)
    MapPopUp_Close: function () {
        $('#map-view').find(".property-map-results-panel").hide();
    },

    //Navigate to the Next page of results on the Map Pop-Up Menu
    MapPopUp_NextPage: function () {
        var pageNumber = parseInt($('#pagination-location-pageNumber').text());
        ListAndMapViewData.MapPopUp_SetPage(pageNumber + 1);
    },

    //Navigate to the Previous page of results on the Map Pop-Up Menu
    MapPopUp_PreviousPage: function () {
        var pageNumber = parseInt($('#pagination-location-pageNumber').text());
        ListAndMapViewData.MapPopUp_SetPage(pageNumber - 1);
    },

    //Sets the visibility of the results on the Map Pop-Up Menu depending on the page being displayed
    //Passed number of the page to display results for</param>
    //Could be modified to get the results required rather than just hiding/showing the results retrieved on initial load?
    MapPopUp_SetPage: function (pageNumber) {
        //var resultElements = $('#map-view .property-search-results').find("li");
        var resultElements = $('#map-view .property-search-results').find(".property-search-result");
        var pageCount = Math.ceil(resultElements.length / ListAndMapViewData.ClusterPageSize);
        pageNumber = parseInt(pageNumber);
        if (isNaN(pageNumber) || pageNumber < 1) {
            pageNumber = 1;
        } else if (pageNumber > pageCount) {
            pageNumber = pageCount;
        }
        var startPos = (ListAndMapViewData.ClusterPageSize * (pageNumber - 1));
        var endPos = startPos + (ListAndMapViewData.ClusterPageSize - 1);

        ListAndMapViewData.Debug_Log('MapPopUp_SetPage(' + pageNumber + ')' +
            '\nResult Count: ' + resultElements.length +
            '\nPage Count: ' + pageCount +
            '\nPage Number: ' + pageNumber +
            '\nPage Size: ' + ListAndMapViewData.ClusterPageSize +
            '\nStart Position: ' + startPos +
            '\nEnd Position: ' + endPos
        );
        resultElements.hide();
        $.each(resultElements, function (idx) {
            if (startPos <= idx && idx <= endPos) {
                $(this).show();
            }
        });
        if (pageCount > 1) {
            $(".property-map-results-panel .property-map-results-summary").show();
            $(".property-map-results-panel .pagination").show();
        } else {
            $(".property-map-results-panel .property-map-results-summary").hide();
            $(".property-map-results-panel .pagination").hide();
        }
        $('#pagination-location-pageCount').text(pageCount);
        $('#pagination-location-pageNumber').text(pageNumber);
    },

    
    Map_ResetZoom: function () {
        ListAndMapViewData.Debug_Log("start: Map_ResetZoom");
        if (ListAndMapViewData.IsMapLoaded()) {

            if (ListAndMapViewData.DefaultLatitude) {
                ListAndMapViewData.Debug_Log("debug: Map_ResetZoom - Zooming to default centre");


                ListAndMapViewData.Map.setCenter(new google.maps.LatLng(ListAndMapViewData.DefaultLatitude, ListAndMapViewData.DefaultLongitude));
                ListAndMapViewData.Map.setZoom(ListAndMapViewData.ZoomLevelInitial);
                
            }

            //Was here working, but have since moved back to mapPlot - see impact.  TODO.
            /*if (ListAndMapViewData.BestFitMap) {
                //Set the initial boundary and zoom to emcompass all our pins.
                var bounds = new google.maps.LatLngBounds();
                for (var i = 0; i < ListAndMapViewData.Markers.length; i++) {
                    bounds.extend(ListAndMapViewData.Markers[i].getPosition());
                }
                ListAndMapViewData.Map.fitBounds(bounds);
            }*/
        }
        ListAndMapViewData.Debug_Log("end: Map_ResetZoom");
    },

    //Checks if map has been dragged out of the England boundary we've set and will move back to the nearest edge.
    Map_CheckBoundaries: function() {
        if (ListAndMapViewData.MaxBounds.contains(ListAndMapViewData.Map.getCenter())) {
            //In bounds so do nothing.
            return;
        }

        // We're out of bounds - Move the map back within the bounds
        var c = ListAndMapViewData.Map.getCenter(),
        x = c.lng(),
        y = c.lat(),
        maxX = ListAndMapViewData.MaxBounds.getNorthEast().lng(),
        maxY = ListAndMapViewData.MaxBounds.getNorthEast().lat(),
        minX = ListAndMapViewData.MaxBounds.getSouthWest().lng(),
        minY = ListAndMapViewData.MaxBounds.getSouthWest().lat();

        if (x < minX) x = minX;
        if (x > maxX) x = maxX;
        if (y < minY) y = minY;
        if (y > maxY) y = maxY;

        ListAndMapViewData.Map.setCenter(new google.maps.LatLng(y, x));
    },

    //Creates a map object in the element with ID "divMap"
    //Triggers plotting results on the map (within the callback or after initialisation)
    Map_CreateMap: function () {
        ListAndMapViewData.Debug_Log("start: Map_CreateMap");
        // Initialise the map
        ListAndMapViewData.Map = new google.maps.Map(document.getElementById("divMap"), {
            center: ListAndMapViewData.DefaultLatitude ? new google.maps.LatLng(ListAndMapViewData.DefaultLatitude, ListAndMapViewData.DefaultLongitude) : null,
            zoom: ListAndMapViewData.ZoomLevelInitial,
            maxZoom: 19, //Max you can zoom in to.
            minZoom: 6, //Furthest you can zoom out.
            scrollwheel: false,
            fullscreenControl: false,
            gestureHandling: "cooperative"
        });


        //Hide the info popup if clicking on the map. - note this also fires when cluster is clicked too (seems like a bug in the cluster as it doesnt seem to register as a marker properly in this case)
        ListAndMapViewData.Map.addListener('click', function (e) {
            //Add handler for the map click event to enable zooming once clicked.
            if (this.scrollwheel === false) {
                this.setOptions({
                    scrollwheel: true
                });
            }
            setTimeout(function () {
                if (!ListAndMapViewData.ClusterClicked) {
                    ListAndMapViewData.MapPopUp_Close();
                }
                else { //Cluster was clicked so set it now to false as the popup should have opened ok and we don't need to do anything else.
                    ListAndMapViewData.ClusterClicked = false;
                }
            }, 0);
        });

        // Listen for the dragend event
        google.maps.event.addListener(ListAndMapViewData.Map, 'dragend', ListAndMapViewData.Map_CheckBoundaries);
            
        // Listen for the mouse out of the map event, to disable the scrollwheel again
        google.maps.event.addListener(ListAndMapViewData.Map, 'mouseout', function (e) {
            //Add handler for the map click event to enable zooming once clicked.
            if (this.scrollwheel === true) {
                this.setOptions({
                    scrollwheel: false
                });
            }
        });


        ListAndMapViewData.Debug_Log("end: Map_CreateMap");
    },

    //Creates a Google marker
    //Passed EH specified JSON object containing geographical data for displaying
    Map_CreateSinglePin: function (data) {
        try {
            var latlng = new google.maps.LatLng(data.Latitude, data.Longitude);
            var iconUrl = "/static/icons/pin-single-property.png";
            //Get our custom pin icon.
            if (ListAndMapViewData.Map_GetSinglePinIcon_Extended) {
                iconUrl = ListAndMapViewData.Map_GetSinglePinIcon_Extended(data);
            }

            var marker = new google.maps.Marker({
                position: latlng,
                map: ListAndMapViewData.Map,
                title: data.Title,
                icon: {
                    url: iconUrl
                    //anchor: new google.maps.Point(26, 36),
                    //labelOrigin: new google.maps.Point(13, 10) //text needs to be in the middle, and moved up a bit so it's in the square portion.
                }
            });
            marker.Item = data; //Send the item through so we can use this on the popup.

            //Add handler for the pushpin click event.
            marker.addListener('click', ListAndMapViewData.MapPopUp_Open);
            return marker;
        }
        catch (err) {
            console.log(err.message + " for " + data.Title + ", path: " + data.Path);
            return null;
        }
    },

    ///<summary>Show/Open the message box</summary>
    MessageBox_Show: function (html) {
        ListAndMapViewData.MapPopUp_Close();
        $("#geocodingPopup")
            .html(html);
        ListAndMapViewData.MessageBox_ShowCallback();
    },

    ///<summary>Shows/Opens the message box one triggered by MessageBox_Show</summary>
    MessageBox_ShowCallback: function () {
        try {
            if ($("#geocodingPopup").foundation) {
                $("#geocodingPopup").foundation('reveal', 'open');
            } else {
                setTimeout(ListAndMapViewData.MessageBox_ShowCallback, 100);
            }
        } catch (ex) { ListAndMapViewData.Debug_Log("Error in MessageBox_ShowCallback: " + JSON.stringify(ex)) }
    },

    ///<summary>Hide/Close the message box</summary>
    MessageBox_Hide: function () {
        try {
            if ($("#geocodingPopup").foundation) {
                $("#geocodingPopup").foundation('reveal', 'close');
            } else {
                setTimeout(ListAndMapViewData.MessageBox_Hide, 100);
            }
        } catch (ex) { ListAndMapViewData.Debug_Log("Error in MessageBox_Hide: " + JSON.stringify(ex)) }
    },

    //Search for item matching criteria on name
    Search_FindMatchingItem: function () {
        ListAndMapViewData.Debug_Log("start: Search_FindMatchingItem");
        ListAndMapViewData.Map_ClearCentrePin();
        var k = 0;
        var searchCriteria = ListAndMapViewData.CentrePinTitle;
        //No search criteria so quit search.
        if (searchCriteria.length == 0) {
            return false;
        }
        for (var i = 0; i < ListAndMapViewData.Results.length; i++) {
            if (searchCriteria.toLowerCase() == ListAndMapViewData.Results[i].Title.toLowerCase()) {
                //Found match on name.
                var item = ListAndMapViewData.Results[i];
                ListAndMapViewData.Search_SetSearchLocationAsCentre(item.Latitude, item.Longitude, item.Title);
                ListAndMapViewData.Debug_Log("end: Search_FindMatchingItem - location = " + item.Title);
                //return true;
                //Do next step needed.
                ListAndMapViewData.ListView_SetPageNumber(1);
                //ListAndMapViewData.ListView_RenderResults();
                return true;
            }

        }
        //If got here then didn't find a match on name.
        ListAndMapViewData.Debug_Log("end: Search_FindMatchingItem");
        return false;
    },

    //DM - Only called if no results match the search criteria.  It then tries to geocode your search text.  So if searching on Swindon
    //it would return the lat long for Swindon using a bing webclient function.  
    Search_GeoCode: function () {
        ListAndMapViewData.Debug_Log("start: Search_GeoCode");
        var searchCriteria = ListAndMapViewData.CentrePinTitle;

        //DM why is this creating the map!? TODO check impact here of commenting out.
        /*if (!ListAndMapViewData.IsMapLoaded()) {
            ListAndMapViewData.Map_CreateMap();
        }*/


        //If we have a lat long as per a near me search then effectively simulate the lat long look up and pass the json object to the 
        //Search_GeoCodeResultsSuccess method.  
        if (ListAndMapViewData.CurrentLatitude != 0 && ListAndMapViewData.CurrentLongitude != 0) {
            //var currentLatLong = { "Results": [{ "Title": "My Location", "Latitude": ListAndMapViewData.CurrentLatitude, "Longitude": ListAndMapViewData.CurrentLongitude }] };
            var currentLatLong = [];
            var currentLatLongObj = { "Title": "My Location", "Latitude": ListAndMapViewData.CurrentLatitude, "Longitude": ListAndMapViewData.CurrentLongitude };
            currentLatLong.push(currentLatLongObj);
            ListAndMapViewData.Search_GeoCodeSuccess(currentLatLong);
        }
        else if (searchCriteria && searchCriteria != "") {
            var url = "/api/Geocode/GetLatLongFromLocation?location=" + searchCriteria;

            $.ajax({
                type: "GET",
                url: url,
                contentType: "application/json; charset=utf-8",
                data: "",
                dataType: "json",
                success: function (msg) { ListAndMapViewData.Search_GeoCodeSuccess(msg); },
                error: function (msg) {
                    if (!ListAndMapViewData.IsListRendered()) { //render html results if we don't have them yet regardless of if geocoding worked.
                        ListAndMapViewData.ListView_RenderResults();
                    }
                    ListAndMapViewData.Debug_Log(msg);
                }
            });
        }
        else {
            //Why plotting data when got no results from geocode!?
            //ListAndMapViewData.Map_PlotData();  
        }
        ListAndMapViewData.Debug_Log("end: Search_GeoCode");
    },

    //Uses lat long from the geo code to go to that location on the map 
    Search_GeoCodeSuccess: function (json) {
        ListAndMapViewData.Debug_Log("start: Search_GeoCodeSuccess");
        var userEnteredText = ListAndMapViewData.CentrePinTitle;
        //DM added null check here as if no results it errors.
        if (json != null && json.length > 0) {
            var results = json;
            if (results && results.push && results.length != 0) {
                if (results.length === 1) {
                    //Zoom to location
                    ListAndMapViewData.Search_SetSearchLocationAsCentre(results[0].Latitude, results[0].Longitude, userEnteredText);
                    //Render list view results.
                    ListAndMapViewData.ListView_SetPageNumber(1);
                } else {
                    //More than 1 came back i.e Swindon, Swindon and Swindon, Glos.
                    var geocodePopupHTML = "<ul>";
                    $.each(results, function (i, item) { 
                        geocodePopupHTML += '<li><a href=\'Javascript: ListAndMapViewData.Search_SetSearchLocationAsCentreAndRenderResults(' + item.Latitude + ',' + item.Longitude + ', "' + item.Title + '");\' title="' + item.Title + '">' + item.Title + '</a></li>';
                    });
                    geocodePopupHTML += "</ul>";
                    geocodePopupHTML = "<h3 class='map'>Did you mean:</h3><br />" + geocodePopupHTML;
                    ListAndMapViewData.Map_ClearCentrePin();
                    ListAndMapViewData.MessageBox_Show(geocodePopupHTML);
                    //Have to have this here in case user doesn't pick an option from the did you mean popup and it is a page load.
                    //Will mean this will run twice though if user does pick a location, but too risky of a blank set of results.
                    if (!ListAndMapViewData.IsListRendered()) {
                        ListAndMapViewData.ListView_RenderResults(); 
                    }
                }
            }
            else {
                ListAndMapViewData.Search_Reset(true);
                ListAndMapViewData.MessageBox_Show('<h3 class="map">There were no results found for the location "' + userEnteredText + '".</h3>');
            }
        }
        else {
            ListAndMapViewData.Search_Reset(true);
            ListAndMapViewData.MessageBox_Show('<h3 class="map">There were no results found for the location "' + userEnteredText + '".</h3>');
        }
        ListAndMapViewData.Debug_Log("end: Search_GeoCodeSuccess");
    },

    Search_ManualRun: function () {
        ListAndMapViewData.Debug_Log("start: Search_ManualRun");
        ListAndMapViewData.MessageBox_Hide();
        ListAndMapViewData.MapPopUp_Close();
        ListAndMapViewData.Json = null;
        ListAndMapViewData.Data_GetJsonResults(0);
        ListAndMapViewData.ListView_ScrollToResultCount();
        ListAndMapViewData.Debug_Log("end: Search_ManualRun");
    },

    Search_Reset: function (persistSearchCriteria) {
        ListAndMapViewData.Debug_Log("start: Search_Reset");
        ListAndMapViewData.MessageBox_Hide();
        ListAndMapViewData.MapPopUp_Close();
        ListAndMapViewData.Map_ClearCentrePin();
        ListAndMapViewData.CurrentLatitude = 0;
        ListAndMapViewData.CurrentLongitude = 0;
        ListAndMapViewData.CentrePinTitle = "";
        
        if (ListAndMapViewData.IsMapLoaded()) {
            ListAndMapViewData.Map_ResetZoom();
            //If using clustering, it can sometimes not quite cluster properly as map hasn't reset it's zoom and redrawn before clustering kicks in.
            //Adding a second delay sorts it.
            if (ListAndMapViewData.UseClustering && ListAndMapViewData.MarkerCluster) {
                setTimeout(function () {
                    ListAndMapViewData.Map_PlotData();
                }, 1000);
            }
            else {
                ListAndMapViewData.Map_PlotData();
            }
            
        }
        if (!persistSearchCriteria) {
            $("#locationSearchField").val(""); //DM Commented out as in debug I'm sure this is firing in another thread which then wipes out our search box.
        }
        ListAndMapViewData.ListView_SetPageNumber(1);

        //If no html is rendered yet then have tried to geocode a result on page load that isn't there, and a url change won't have taken affect to force the render list html.
        if (!ListAndMapViewData.IsListRendered()) {
            ListAndMapViewData.ListView_RenderResults();
        }
        ListAndMapViewData.Debug_Log("end: Search_Reset");
    },

    //Runs the search against the search box at the top of the page.  
    Search_RunSearch: function () {
        ListAndMapViewData.Debug_Log("start: Search_RunSearch");

        ListAndMapViewData.MessageBox_Hide();
        ListAndMapViewData.MapPopUp_Close();

        //Search a property matching the search criteria text, if found it goes off and does the rest.
        var foundMatchOnItem = ListAndMapViewData.Search_FindMatchingItem();
        //If it's not found then geocode the search criteria instead, as long as this option has not be overriden.
        if (!foundMatchOnItem) {
            if (!ListAndMapViewData.GeocodeLatLongWhenGetData) {
                ListAndMapViewData.Search_GeoCode();
            }
        }
        /* don't know why plot data was here really! */
        /*else {
            ListAndMapViewData.Map_PlotData(); //DM force it to reload the location pin icons as sometimes they go missing when searching for different names back to back.
        }*/
        ListAndMapViewData.Debug_Log("end: Search_RunSearch");
    },

    ///Loads parameters from querystring to set data on loading or paging. 
    SearchParams_Load: function () {
        var pageNumber = parseInt(ListAndMapViewData.Url_GetParameterByName("page"));
        pageNumber = (isNaN(pageNumber) ? 1 : pageNumber);

        //Load any custom parameters.
        if (ListAndMapViewData.SearchParams_Load_Extended) {
            ListAndMapViewData.SearchParams_Load_Extended();
        }
    },

    ///<summary>Get the value from the location hash</summary>
    Url_GetParameterByName: function (name) {
        var match = RegExp('[?&]' + name + '=([^&]*)', 'i').exec(window.location.hash);
        var returnValue = match && decodeURIComponent(match[1].replace(/\+/g, ' '));
        return (returnValue ? returnValue : "");
    },

    ///<summary>Code to call when the location hash has changed</summary>
    ///<remarks>Refreshes the list view</remarks>
    Url_LocationHashChanged: function () {
        ListAndMapViewData.Debug_Log("start: Url_LocationHashChanged");
        ListAndMapViewData.ListView_RenderResults();
        ListAndMapViewData.Debug_Log("end: Url_LocationHashChanged");
    },


    //Update the location hash
    Url_SetLocationHash: function (pageNumberDS1, pageNumberDS2) {
        ListAndMapViewData.Debug_Log("start: Url_SetLocationHash");
        var currentLocationHash = window.location.hash;
        if (pageNumberDS2 == null) { //No second set of page numbers so standard single dataset.        
            window.location.hash = encodeURI(ListAndMapViewData.Url_GetQueryString_Extended(pageNumberDS1));
        }
        else { //Both numbers have a value so we are doing two data sets.
            window.location.hash = encodeURI(ListAndMapViewData.Url_GetQueryString_Extended(pageNumberDS1, pageNumberDS2));
        }
        //Add code to check if location hash hasn't changed, then force it to redraw listview results (for page reloads).
        if (currentLocationHash == window.location.hash) {
            ListAndMapViewData.ListView_RenderResults();
        }

        ListAndMapViewData.Debug_Log("end: Url_SetLocationHash");
    }

}

//Google file from https://github.com/googlemaps/v3-utility-library/tree/master/markerclusterer
//DM has had to write 2 bug fixes to make it work for us.  Marked with //DM comment.

// ==ClosureCompiler==
// @compilation_level ADVANCED_OPTIMIZATIONS
// @externs_url http://closure-compiler.googlecode.com/svn/trunk/contrib/externs/maps/google_maps_api_v3_3.js
// ==/ClosureCompiler==

/**
 * @name MarkerClusterer for Google Maps v3
 * @version version 1.0.3
 * @author Luke Mahe
 * @fileoverview
 * The library creates and manages per-zoom-level clusters for large amounts of
 * markers.
 */

/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


/**
 * A Marker Clusterer that clusters markers.
 *
 * @param {google.maps.Map} map The Google map to attach to.
 * @param {Array.<google.maps.Marker>=} opt_markers Optional markers to add to
 *   the cluster.
 * @param {Object=} opt_options support the following options:
 *     'gridSize': (number) The grid size of a cluster in pixels.
 *     'maxZoom': (number) The maximum zoom level that a marker can be part of a
 *                cluster.
 *     'zoomOnClick': (boolean) Whether the default behaviour of clicking on a
 *                    cluster is to zoom into it.
 *     'imagePath': (string) The base URL where the images representing
 *                  clusters will be found. The full URL will be:
 *                  {imagePath}[1-5].{imageExtension}
 *                  Default: '../images/m'.
 *     'imageExtension': (string) The suffix for images URL representing
 *                       clusters will be found. See _imagePath_ for details.
 *                       Default: 'png'.
 *     'averageCenter': (boolean) Whether the center of each cluster should be
 *                      the average of all markers in the cluster.
 *     'minimumClusterSize': (number) The minimum number of markers to be in a
 *                           cluster before the markers are hidden and a count
 *                           is shown.
 *     'styles': (object) An object that has style properties:
 *       'url': (string) The image url.
 *       'height': (number) The image height.
 *       'width': (number) The image width.
 *       'anchor': (Array) The anchor position of the label text.
 *       'textColor': (string) The text color.
 *       'textSize': (number) The text size.
 *       'backgroundPosition': (string) The position of the backgound x, y.
 * @constructor
 * @extends google.maps.OverlayView
 */
function MarkerClusterer(map, opt_markers, opt_options) {
  // MarkerClusterer implements google.maps.OverlayView interface. We use the
  // extend function to extend MarkerClusterer with google.maps.OverlayView
  // because it might not always be available when the code is defined so we
  // look for it at the last possible moment. If it doesn't exist now then
  // there is no point going ahead :)
  this.extend(MarkerClusterer, google.maps.OverlayView);
  this.map_ = map;

  /**
   * @type {Array.<google.maps.Marker>}
   * @private
   */
  this.markers_ = [];

  /**
   *  @type {Array.<Cluster>}
   */
  this.clusters_ = [];

  this.sizes = [53, 56, 66, 78, 90];

  /**
   * @private
   */
  this.styles_ = [];

  /**
   * @type {boolean}
   * @private
   */
  this.ready_ = false;

  var options = opt_options || {};

  /**
   * @type {number}
   * @private
   */
  this.gridSize_ = options['gridSize'] || 60;

  /**
   * @private
   */
  this.minClusterSize_ = options['minimumClusterSize'] || 2;


  /**
   * @type {?number}
   * @private
   */
  this.maxZoom_ = options['maxZoom'] || null;

  this.styles_ = options['styles'] || [];

  /**
   * @type {string}
   * @private
   */
  this.imagePath_ = options['imagePath'] ||
      this.MARKER_CLUSTER_IMAGE_PATH_;

  /**
   * @type {string}
   * @private
   */
  this.imageExtension_ = options['imageExtension'] ||
      this.MARKER_CLUSTER_IMAGE_EXTENSION_;

  /**
   * @type {boolean}
   * @private
   */
  this.zoomOnClick_ = true;

  if (options['zoomOnClick'] != undefined) {
    this.zoomOnClick_ = options['zoomOnClick'];
  }

  /**
   * @type {boolean}
   * @private
   */
  this.averageCenter_ = false;

  if (options['averageCenter'] != undefined) {
    this.averageCenter_ = options['averageCenter'];
  }

  this.setupStyles_();

  this.setMap(map);

  /**
   * @type {number}
   * @private
   */
  this.prevZoom_ = this.map_.getZoom();

  // Add the map event listeners
  var that = this;
  google.maps.event.addListener(this.map_, 'zoom_changed', function() {
    // Determines map type and prevent illegal zoom levels
    var zoom = that.map_.getZoom();
    var minZoom = that.map_.minZoom || 0;

    var maxZoom = Math.min(that.map_.maxZoom || 100,
                         //that.map_.mapTypes[that.map_.getMapTypeId()].maxZoom);  //DM had to fix this as maptypes is undefined when searching for property and then clicking the map tab if you've not loaded map tab first.
                that.map_.mapTypes[that.map_.getMapTypeId()] == undefined ? 100 : that.map_.mapTypes[that.map_.getMapTypeId()].maxZoom);
    zoom = Math.min(Math.max(zoom,minZoom),maxZoom);

    if (that.prevZoom_ != zoom) {
      that.prevZoom_ = zoom;
      that.resetViewport();
    }
  });

  google.maps.event.addListener(this.map_, 'idle', function() {
    that.redraw();
  });

  // Finally, add the markers
  if (opt_markers && (opt_markers.length || Object.keys(opt_markers).length)) {
    this.addMarkers(opt_markers, false);
  }
}


/**
 * The marker cluster image path.
 *
 * @type {string}
 * @private
 */
MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m';


/**
 * The marker cluster image path.
 *
 * @type {string}
 * @private
 */
MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png';


/**
 * Extends a objects prototype by anothers.
 *
 * @param {Object} obj1 The object to be extended.
 * @param {Object} obj2 The object to extend with.
 * @return {Object} The new extended object.
 * @ignore
 */
MarkerClusterer.prototype.extend = function(obj1, obj2) {
  return (function(object) {
    for (var property in object.prototype) {
      this.prototype[property] = object.prototype[property];
    }
    return this;
  }).apply(obj1, [obj2]);
};


/**
 * Implementaion of the interface method.
 * @ignore
 */
MarkerClusterer.prototype.onAdd = function() {
  this.setReady_(true);
};

/**
 * Implementaion of the interface method.
 * @ignore
 */
MarkerClusterer.prototype.draw = function() {};

/**
 * Sets up the styles object.
 *
 * @private
 */
MarkerClusterer.prototype.setupStyles_ = function() {
  if (this.styles_.length) {
    return;
  }

  for (var i = 0, size; size = this.sizes[i]; i++) {
    this.styles_.push({
      url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_,
      height: size,
      width: size
    });
  }
};

/**
 *  Fit the map to the bounds of the markers in the clusterer.
 */
MarkerClusterer.prototype.fitMapToMarkers = function() {
  var markers = this.getMarkers();
  var bounds = new google.maps.LatLngBounds();
  for (var i = 0, marker; marker = markers[i]; i++) {
    bounds.extend(marker.getPosition());
  }

  this.map_.fitBounds(bounds);
};


/**
 *  Sets the styles.
 *
 *  @param {Object} styles The style to set.
 */
MarkerClusterer.prototype.setStyles = function(styles) {
  this.styles_ = styles;
};


/**
 *  Gets the styles.
 *
 *  @return {Object} The styles object.
 */
MarkerClusterer.prototype.getStyles = function() {
  return this.styles_;
};


/**
 * Whether zoom on click is set.
 *
 * @return {boolean} True if zoomOnClick_ is set.
 */
MarkerClusterer.prototype.isZoomOnClick = function() {
  return this.zoomOnClick_;
};

/**
 * Whether average center is set.
 *
 * @return {boolean} True if averageCenter_ is set.
 */
MarkerClusterer.prototype.isAverageCenter = function() {
  return this.averageCenter_;
};


/**
 *  Returns the array of markers in the clusterer.
 *
 *  @return {Array.<google.maps.Marker>} The markers.
 */
MarkerClusterer.prototype.getMarkers = function() {
  return this.markers_;
};


/**
 *  Returns the number of markers in the clusterer
 *
 *  @return {Number} The number of markers.
 */
MarkerClusterer.prototype.getTotalMarkers = function() {
  return this.markers_.length;
};


/**
 *  Sets the max zoom for the clusterer.
 *
 *  @param {number} maxZoom The max zoom level.
 */
MarkerClusterer.prototype.setMaxZoom = function(maxZoom) {
  this.maxZoom_ = maxZoom;
};


/**
 *  Gets the max zoom for the clusterer.
 *
 *  @return {number} The max zoom level.
 */
MarkerClusterer.prototype.getMaxZoom = function() {
  return this.maxZoom_;
};


/**
 *  The function for calculating the cluster icon image.
 *
 *  @param {Array.<google.maps.Marker>} markers The markers in the clusterer.
 *  @param {number} numStyles The number of styles available.
 *  @return {Object} A object properties: 'text' (string) and 'index' (number).
 *  @private
 */
MarkerClusterer.prototype.calculator_ = function(markers, numStyles) {
  var index = 0;
  var count = markers.length;
  var dv = count;
  while (dv !== 0) {
    dv = parseInt(dv / 10, 10);
    index++;
  }

  index = Math.min(index, numStyles);
  return {
    text: count,
    index: index
  };
};


/**
 * Set the calculator function.
 *
 * @param {function(Array, number)} calculator The function to set as the
 *     calculator. The function should return a object properties:
 *     'text' (string) and 'index' (number).
 *
 */
MarkerClusterer.prototype.setCalculator = function(calculator) {
  this.calculator_ = calculator;
};


/**
 * Get the calculator function.
 *
 * @return {function(Array, number)} the calculator function.
 */
MarkerClusterer.prototype.getCalculator = function() {
  return this.calculator_;
};


/**
 * Add an array of markers to the clusterer.
 *
 * @param {Array.<google.maps.Marker>} markers The markers to add.
 * @param {boolean=} opt_nodraw Whether to redraw the clusters.
 */
MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) {
  if (markers.length) {
    for (var i = 0, marker; marker = markers[i]; i++) {
      this.pushMarkerTo_(marker);
    }
  } else if (Object.keys(markers).length) {
    for (var marker in markers) {
      this.pushMarkerTo_(markers[marker]);
    }
  }
  if (!opt_nodraw) {
    this.redraw();
  }
};


/**
 * Pushes a marker to the clusterer.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @private
 */
MarkerClusterer.prototype.pushMarkerTo_ = function(marker) {
  marker.isAdded = false;
  if (marker['draggable']) {
    // If the marker is draggable add a listener so we update the clusters on
    // the drag end.
    var that = this;
    google.maps.event.addListener(marker, 'dragend', function() {
      marker.isAdded = false;
      that.repaint();
    });
  }
  this.markers_.push(marker);
};


/**
 * Adds a marker to the clusterer and redraws if needed.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @param {boolean=} opt_nodraw Whether to redraw the clusters.
 */
MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) {
  this.pushMarkerTo_(marker);
  if (!opt_nodraw) {
    this.redraw();
  }
};


/**
 * Removes a marker and returns true if removed, false if not
 *
 * @param {google.maps.Marker} marker The marker to remove
 * @return {boolean} Whether the marker was removed or not
 * @private
 */
MarkerClusterer.prototype.removeMarker_ = function(marker) {
  var index = -1;
  if (this.markers_.indexOf) {
    index = this.markers_.indexOf(marker);
  } else {
    for (var i = 0, m; m = this.markers_[i]; i++) {
      if (m == marker) {
        index = i;
        break;
      }
    }
  }

  if (index == -1) {
    // Marker is not in our list of markers.
    return false;
  }

  marker.setMap(null);

  this.markers_.splice(index, 1);

  return true;
};


/**
 * Remove a marker from the cluster.
 *
 * @param {google.maps.Marker} marker The marker to remove.
 * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
 * @return {boolean} True if the marker was removed.
 */
MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) {
  var removed = this.removeMarker_(marker);

  if (!opt_nodraw && removed) {
    this.resetViewport();
    this.redraw();
    return true;
  } else {
   return false;
  }
};


/**
 * Removes an array of markers from the cluster.
 *
 * @param {Array.<google.maps.Marker>} markers The markers to remove.
 * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
 */
MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) {
  // create a local copy of markers if required
  // (removeMarker_ modifies the getMarkers() array in place)
  var markersCopy = markers === this.getMarkers() ? markers.slice() : markers;
  var removed = false;

  for (var i = 0, marker; marker = markersCopy[i]; i++) {
    var r = this.removeMarker_(marker);
    removed = removed || r;
  }

  if (!opt_nodraw && removed) {
    this.resetViewport();
    this.redraw();
    return true;
  }
};


/**
 * Sets the clusterer's ready state.
 *
 * @param {boolean} ready The state.
 * @private
 */
MarkerClusterer.prototype.setReady_ = function(ready) {
  if (!this.ready_) {
    this.ready_ = ready;
    this.createClusters_();
  }
};


/**
 * Returns the number of clusters in the clusterer.
 *
 * @return {number} The number of clusters.
 */
MarkerClusterer.prototype.getTotalClusters = function() {
  return this.clusters_.length;
};


/**
 * Returns the google map that the clusterer is associated with.
 *
 * @return {google.maps.Map} The map.
 */
MarkerClusterer.prototype.getMap = function() {
  return this.map_;
};


/**
 * Sets the google map that the clusterer is associated with.
 *
 * @param {google.maps.Map} map The map.
 */
MarkerClusterer.prototype.setMap = function(map) {
  this.map_ = map;
};


/**
 * Returns the size of the grid.
 *
 * @return {number} The grid size.
 */
MarkerClusterer.prototype.getGridSize = function() {
  return this.gridSize_;
};


/**
 * Sets the size of the grid.
 *
 * @param {number} size The grid size.
 */
MarkerClusterer.prototype.setGridSize = function(size) {
  this.gridSize_ = size;
};


/**
 * Returns the min cluster size.
 *
 * @return {number} The grid size.
 */
MarkerClusterer.prototype.getMinClusterSize = function() {
  return this.minClusterSize_;
};

/**
 * Sets the min cluster size.
 *
 * @param {number} size The grid size.
 */
MarkerClusterer.prototype.setMinClusterSize = function(size) {
  this.minClusterSize_ = size;
};


/**
 * Extends a bounds object by the grid size.
 *
 * @param {google.maps.LatLngBounds} bounds The bounds to extend.
 * @return {google.maps.LatLngBounds} The extended bounds.
 */
MarkerClusterer.prototype.getExtendedBounds = function(bounds) {
  var projection = this.getProjection();

  // Turn the bounds into latlng.
  var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
      bounds.getNorthEast().lng());
  var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
      bounds.getSouthWest().lng());

  // Convert the points to pixels and the extend out by the grid size.
  var trPix = projection.fromLatLngToDivPixel(tr);
  trPix.x += this.gridSize_;
  trPix.y -= this.gridSize_;

  var blPix = projection.fromLatLngToDivPixel(bl);
  blPix.x -= this.gridSize_;
  blPix.y += this.gridSize_;

  // Convert the pixel points back to LatLng
  var ne = projection.fromDivPixelToLatLng(trPix);
  var sw = projection.fromDivPixelToLatLng(blPix);

  // Extend the bounds to contain the new bounds.
  bounds.extend(ne);
  bounds.extend(sw);

  return bounds;
};


/**
 * Determins if a marker is contained in a bounds.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @param {google.maps.LatLngBounds} bounds The bounds to check against.
 * @return {boolean} True if the marker is in the bounds.
 * @private
 */
MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) {
  return bounds.contains(marker.getPosition());
};


/**
 * Clears all clusters and markers from the clusterer.
 */
MarkerClusterer.prototype.clearMarkers = function() {
  this.resetViewport(true);

  // Set the markers a empty array.
  this.markers_ = [];
};


/**
 * Clears all existing clusters and recreates them.
 * @param {boolean} opt_hide To also hide the marker.
 */
MarkerClusterer.prototype.resetViewport = function(opt_hide) {
  // Remove all the clusters
  for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
    cluster.remove();
  }

  // Reset the markers to not be added and to be invisible.
  for (var i = 0, marker; marker = this.markers_[i]; i++) {
    marker.isAdded = false;
    if (opt_hide) {
      marker.setMap(null);
    }
  }

  this.clusters_ = [];
};

/**
 *
 */
MarkerClusterer.prototype.repaint = function() {
  var oldClusters = this.clusters_.slice();
  this.clusters_.length = 0;
  this.resetViewport();
  this.redraw();

  // Remove the old clusters.
  // Do it in a timeout so the other clusters have been drawn first.
  window.setTimeout(function() {
    for (var i = 0, cluster; cluster = oldClusters[i]; i++) {
      cluster.remove();
    }
  }, 0);
};


/**
 * Redraws the clusters.
 */
MarkerClusterer.prototype.redraw = function() {
  this.createClusters_();
};


/**
 * Calculates the distance between two latlng locations in km.
 * @see http://www.movable-type.co.uk/scripts/latlong.html
 *
 * @param {google.maps.LatLng} p1 The first lat lng point.
 * @param {google.maps.LatLng} p2 The second lat lng point.
 * @return {number} The distance between the two points in km.
 * @private
*/
MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) {
  if (!p1 || !p2) {
    return 0;
  }

  var R = 6371; // Radius of the Earth in km
  var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
  var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
  var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * 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;
};


/**
 * Add a marker to a cluster, or creates a new cluster.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @private
 */
MarkerClusterer.prototype.addToClosestCluster_ = function(marker) {
  var distance = 40000; // Some large number
  var clusterToAddTo = null;
  var pos = marker.getPosition();
  for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
    var center = cluster.getCenter();
    if (center) {
      var d = this.distanceBetweenPoints_(center, marker.getPosition());
      if (d < distance) {
        distance = d;
        clusterToAddTo = cluster;
      }
    }
  }

  if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
    clusterToAddTo.addMarker(marker);
  } else {
    var cluster = new Cluster(this);
    cluster.addMarker(marker);
    this.clusters_.push(cluster);
  }
};


/**
 * Creates the clusters.
 *
 * @private
 */
MarkerClusterer.prototype.createClusters_ = function() {
  if (!this.ready_) {
    return;
  }

  // Get our current map view bounds.
  // Create a new bounds object so we don't affect the map.
  var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(),
      this.map_.getBounds().getNorthEast());
  var bounds = this.getExtendedBounds(mapBounds);

  for (var i = 0, marker; marker = this.markers_[i]; i++) {
    if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
      this.addToClosestCluster_(marker);
    }
  }
};


/**
 * A cluster that contains markers.
 *
 * @param {MarkerClusterer} markerClusterer The markerclusterer that this
 *     cluster is associated with.
 * @constructor
 * @ignore
 */
function Cluster(markerClusterer) {
  this.markerClusterer_ = markerClusterer;
  this.map_ = markerClusterer.getMap();
  this.gridSize_ = markerClusterer.getGridSize();
  this.minClusterSize_ = markerClusterer.getMinClusterSize();
  this.averageCenter_ = markerClusterer.isAverageCenter();
  this.center_ = null;
  this.markers_ = [];
  this.bounds_ = null;
  this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(),
      markerClusterer.getGridSize());
}

/**
 * Determins if a marker is already added to the cluster.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @return {boolean} True if the marker is already added.
 */
Cluster.prototype.isMarkerAlreadyAdded = function(marker) {
  if (this.markers_.indexOf) {
    return this.markers_.indexOf(marker) != -1;
  } else {
    for (var i = 0, m; m = this.markers_[i]; i++) {
      if (m == marker) {
        return true;
      }
    }
  }
  return false;
};


/**
 * Add a marker the cluster.
 *
 * @param {google.maps.Marker} marker The marker to add.
 * @return {boolean} True if the marker was added.
 */
Cluster.prototype.addMarker = function(marker) {
  if (this.isMarkerAlreadyAdded(marker)) {
    return false;
  }

  if (!this.center_) {
    this.center_ = marker.getPosition();
    this.calculateBounds_();
  } else {
    if (this.averageCenter_) {
      var l = this.markers_.length + 1;
      var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l;
      var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l;
      this.center_ = new google.maps.LatLng(lat, lng);
      this.calculateBounds_();
    }
  }

  marker.isAdded = true;
  this.markers_.push(marker);

  var len = this.markers_.length;
  if (len < this.minClusterSize_ && marker.getMap() != this.map_) {
    // Min cluster size not reached so show the marker.
    marker.setMap(this.map_);
  }

  if (len == this.minClusterSize_) {
    // Hide the markers that were showing.
    for (var i = 0; i < len; i++) {
      this.markers_[i].setMap(null);
    }
  }

  if (len >= this.minClusterSize_) {
    marker.setMap(null);
  }

  this.updateIcon();
  return true;
};


/**
 * Returns the marker clusterer that the cluster is associated with.
 *
 * @return {MarkerClusterer} The associated marker clusterer.
 */
Cluster.prototype.getMarkerClusterer = function() {
  return this.markerClusterer_;
};


/**
 * Returns the bounds of the cluster.
 *
 * @return {google.maps.LatLngBounds} the cluster bounds.
 */
Cluster.prototype.getBounds = function() {
  var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
  var markers = this.getMarkers();
  for (var i = 0, marker; marker = markers[i]; i++) {
    bounds.extend(marker.getPosition());
  }
  return bounds;
};


/**
 * Removes the cluster
 */
Cluster.prototype.remove = function() {
  this.clusterIcon_.remove();
  this.markers_.length = 0;
  delete this.markers_;
};


/**
 * Returns the number of markers in the cluster.
 *
 * @return {number} The number of markers in the cluster.
 */
Cluster.prototype.getSize = function() {
  return this.markers_.length;
};


/**
 * Returns a list of the markers in the cluster.
 *
 * @return {Array.<google.maps.Marker>} The markers in the cluster.
 */
Cluster.prototype.getMarkers = function() {
  return this.markers_;
};


/**
 * Returns the center of the cluster.
 *
 * @return {google.maps.LatLng} The cluster center.
 */
Cluster.prototype.getCenter = function() {
  return this.center_;
};


/**
 * Calculated the extended bounds of the cluster with the grid.
 *
 * @private
 */
Cluster.prototype.calculateBounds_ = function() {
  var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
  this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
};


/**
 * Determines if a marker lies in the clusters bounds.
 *
 * @param {google.maps.Marker} marker The marker to check.
 * @return {boolean} True if the marker lies in the bounds.
 */
Cluster.prototype.isMarkerInClusterBounds = function(marker) {
  return this.bounds_.contains(marker.getPosition());
};


/**
 * Returns the map that the cluster is associated with.
 *
 * @return {google.maps.Map} The map.
 */
Cluster.prototype.getMap = function() {
  return this.map_;
};


/**
 * Updates the cluster icon
 */
Cluster.prototype.updateIcon = function() {
  var zoom = this.map_.getZoom();
  var mz = this.markerClusterer_.getMaxZoom();

  if (mz && zoom > mz) {
    // The zoom is greater than our max zoom so show all the markers in cluster.
    for (var i = 0, marker; marker = this.markers_[i]; i++) {
      marker.setMap(this.map_);
    }
    return;
  }

  if (this.markers_.length < this.minClusterSize_) {
    // Min cluster size not yet reached.
    this.clusterIcon_.hide();
    return;
  }

  var numStyles = this.markerClusterer_.getStyles().length;
  var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
  this.clusterIcon_.setCenter(this.center_);
  this.clusterIcon_.setSums(sums);
  this.clusterIcon_.show();
};


/**
 * A cluster icon
 *
 * @param {Cluster} cluster The cluster to be associated with.
 * @param {Object} styles An object that has style properties:
 *     'url': (string) The image url.
 *     'height': (number) The image height.
 *     'width': (number) The image width.
 *     'anchor': (Array) The anchor position of the label text.
 *     'textColor': (string) The text color.
 *     'textSize': (number) The text size.
 *     'backgroundPosition: (string) The background postition x, y.
 * @param {number=} opt_padding Optional padding to apply to the cluster icon.
 * @constructor
 * @extends google.maps.OverlayView
 * @ignore
 */
function ClusterIcon(cluster, styles, opt_padding) {
  cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);

  this.styles_ = styles;
  this.padding_ = opt_padding || 0;
  this.cluster_ = cluster;
  this.center_ = null;
  this.map_ = cluster.getMap();
  this.div_ = null;
  this.sums_ = null;
  this.visible_ = false;

  this.setMap(this.map_);
}


/**
 * Triggers the clusterclick event and zoom's if the option is set.
 */
ClusterIcon.prototype.triggerClusterClick = function() {
  var markerClusterer = this.cluster_.getMarkerClusterer();

  // Trigger the clusterclick event.
  google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_); //DM fixed bug where it had it as markerCluster.map_ which didn't work!!

  if (markerClusterer.isZoomOnClick()) {
    // Zoom into the cluster.
    this.map_.fitBounds(this.cluster_.getBounds());
  }
};


/**
 * Adding the cluster icon to the dom.
 * @ignore
 */
ClusterIcon.prototype.onAdd = function() {
  this.div_ = document.createElement('DIV');
  if (this.visible_) {
    var pos = this.getPosFromLatLng_(this.center_);
    this.div_.style.cssText = this.createCss(pos);
    this.div_.innerHTML = this.sums_.text;
  }

  var panes = this.getPanes();
  panes.overlayMouseTarget.appendChild(this.div_);

  var that = this;
  google.maps.event.addDomListener(this.div_, 'click', function() {
    that.triggerClusterClick();
  });
};


/**
 * Returns the position to place the div dending on the latlng.
 *
 * @param {google.maps.LatLng} latlng The position in latlng.
 * @return {google.maps.Point} The position in pixels.
 * @private
 */
ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) {
  var pos = this.getProjection().fromLatLngToDivPixel(latlng);
  pos.x -= parseInt(this.width_ / 2, 10);
  pos.y -= parseInt(this.height_ / 2, 10);
  return pos;
};


/**
 * Draw the icon.
 * @ignore
 */
ClusterIcon.prototype.draw = function() {
  if (this.visible_) {
    var pos = this.getPosFromLatLng_(this.center_);
    this.div_.style.top = pos.y + 'px';
    this.div_.style.left = pos.x + 'px';
    this.div_.style.zIndex = google.maps.Marker.MAX_ZINDEX + 1;
  }
};


/**
 * Hide the icon.
 */
ClusterIcon.prototype.hide = function() {
  if (this.div_) {
    this.div_.style.display = 'none';
  }
  this.visible_ = false;
};


/**
 * Position and show the icon.
 */
ClusterIcon.prototype.show = function() {
  if (this.div_) {
    var pos = this.getPosFromLatLng_(this.center_);
    this.div_.style.cssText = this.createCss(pos);
    this.div_.style.display = '';
  }
  this.visible_ = true;
};


/**
 * Remove the icon from the map
 */
ClusterIcon.prototype.remove = function() {
  this.setMap(null);
};


/**
 * Implementation of the onRemove interface.
 * @ignore
 */
ClusterIcon.prototype.onRemove = function() {
  if (this.div_ && this.div_.parentNode) {
    this.hide();
    this.div_.parentNode.removeChild(this.div_);
    this.div_ = null;
  }
};


/**
 * Set the sums of the icon.
 *
 * @param {Object} sums The sums containing:
 *   'text': (string) The text to display in the icon.
 *   'index': (number) The style index of the icon.
 */
ClusterIcon.prototype.setSums = function(sums) {
  this.sums_ = sums;
  this.text_ = sums.text;
  this.index_ = sums.index;
  if (this.div_) {
    this.div_.innerHTML = sums.text;
  }

  this.useStyle();
};


/**
 * Sets the icon to the the styles.
 */
ClusterIcon.prototype.useStyle = function() {
  var index = Math.max(0, this.sums_.index - 1);
  index = Math.min(this.styles_.length - 1, index);
  var style = this.styles_[index];
  this.url_ = style['url'];
  this.height_ = style['height'];
  this.width_ = style['width'];
  this.textColor_ = style['textColor'];
  this.anchor_ = style['anchor'];
  this.textSize_ = style['textSize'];
  this.backgroundPosition_ = style['backgroundPosition'];
};


/**
 * Sets the center of the icon.
 *
 * @param {google.maps.LatLng} center The latlng to set as the center.
 */
ClusterIcon.prototype.setCenter = function(center) {
  this.center_ = center;
};


/**
 * Create the css text based on the position of the icon.
 *
 * @param {google.maps.Point} pos The position.
 * @return {string} The css style text.
 */
ClusterIcon.prototype.createCss = function(pos) {
  var style = [];
  style.push('background-image:url(' + this.url_ + ');');
  var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0';
  style.push('background-position:' + backgroundPosition + ';');

  if (typeof this.anchor_ === 'object') {
    if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
        this.anchor_[0] < this.height_) {
      style.push('height:' + (this.height_ - this.anchor_[0]) +
          'px; padding-top:' + this.anchor_[0] + 'px;');
    } else {
      style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
          'px;');
    }
    if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
        this.anchor_[1] < this.width_) {
      style.push('width:' + (this.width_ - this.anchor_[1]) +
          'px; padding-left:' + this.anchor_[1] + 'px;');
    } else {
      style.push('width:' + this.width_ + 'px; text-align:center;');
    }
  } else {
    style.push('height:' + this.height_ + 'px; line-height:' +
        this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
  }

  var txtColor = this.textColor_ ? this.textColor_ : 'black';
  var txtSize = this.textSize_ ? this.textSize_ : 11;

  style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
      pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' +
      txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold');
  return style.join('');
};


// Export Symbols for Closure
// If you are not going to compile with closure then you can remove the
// code below.
var window = window || {};
window['MarkerClusterer'] = MarkerClusterer;
MarkerClusterer.prototype['addMarker'] = MarkerClusterer.prototype.addMarker;
MarkerClusterer.prototype['addMarkers'] = MarkerClusterer.prototype.addMarkers;
MarkerClusterer.prototype['clearMarkers'] =
    MarkerClusterer.prototype.clearMarkers;
MarkerClusterer.prototype['fitMapToMarkers'] =
    MarkerClusterer.prototype.fitMapToMarkers;
MarkerClusterer.prototype['getCalculator'] =
    MarkerClusterer.prototype.getCalculator;
MarkerClusterer.prototype['getGridSize'] =
    MarkerClusterer.prototype.getGridSize;
MarkerClusterer.prototype['getExtendedBounds'] =
    MarkerClusterer.prototype.getExtendedBounds;
MarkerClusterer.prototype['getMap'] = MarkerClusterer.prototype.getMap;
MarkerClusterer.prototype['getMarkers'] = MarkerClusterer.prototype.getMarkers;
MarkerClusterer.prototype['getMaxZoom'] = MarkerClusterer.prototype.getMaxZoom;
MarkerClusterer.prototype['getStyles'] = MarkerClusterer.prototype.getStyles;
MarkerClusterer.prototype['getTotalClusters'] =
    MarkerClusterer.prototype.getTotalClusters;
MarkerClusterer.prototype['getTotalMarkers'] =
    MarkerClusterer.prototype.getTotalMarkers;
MarkerClusterer.prototype['redraw'] = MarkerClusterer.prototype.redraw;
MarkerClusterer.prototype['removeMarker'] =
    MarkerClusterer.prototype.removeMarker;
MarkerClusterer.prototype['removeMarkers'] =
    MarkerClusterer.prototype.removeMarkers;
MarkerClusterer.prototype['resetViewport'] =
    MarkerClusterer.prototype.resetViewport;
MarkerClusterer.prototype['repaint'] =
    MarkerClusterer.prototype.repaint;
MarkerClusterer.prototype['setCalculator'] =
    MarkerClusterer.prototype.setCalculator;
MarkerClusterer.prototype['setGridSize'] =
    MarkerClusterer.prototype.setGridSize;
MarkerClusterer.prototype['setMaxZoom'] =
    MarkerClusterer.prototype.setMaxZoom;
MarkerClusterer.prototype['onAdd'] = MarkerClusterer.prototype.onAdd;
MarkerClusterer.prototype['draw'] = MarkerClusterer.prototype.draw;

Cluster.prototype['getCenter'] = Cluster.prototype.getCenter;
Cluster.prototype['getSize'] = Cluster.prototype.getSize;
Cluster.prototype['getMarkers'] = Cluster.prototype.getMarkers;

ClusterIcon.prototype['onAdd'] = ClusterIcon.prototype.onAdd;
ClusterIcon.prototype['draw'] = ClusterIcon.prototype.draw;
ClusterIcon.prototype['onRemove'] = ClusterIcon.prototype.onRemove;

Object.keys = Object.keys || function(o) {
    var result = [];
    for(var name in o) {
        if (o.hasOwnProperty(name))
          result.push(name);
    }
    return result;
};

if (typeof module == 'object') {
  module.exports = MarkerClusterer;
}

