Source: src/js/views/DataCatalogView.js

/*global define */
define(["jquery",
        "underscore",
        "backbone",
        "collections/SolrResults",
        "models/Search",
        "models/MetricsModel",
        "common/Utilities",
        "views/SearchResultView",
        "views/searchSelect/AnnotationFilterView",
        "text!templates/search.html",
        "text!templates/statCounts.html",
        "text!templates/pager.html",
        "text!templates/mainContent.html",
        "text!templates/currentFilter.html",
        "text!templates/loading.html",
        "gmaps",
        "nGeohash"
    ],
    function(
      $, _, Backbone, SearchResults, SearchModel,
        MetricsModel, Utilities, SearchResultView, AnnotationFilter,
        CatalogTemplate, CountTemplate, PagerTemplate, MainContentTemplate,
        CurrentFilterTemplate, LoadingTemplate, gmaps, nGeohash
    ) {
        "use strict";

        /**
        * @class DataCatalogView
        * @classcategory Views
        * @extends Backbone.View
        * @constructor
        * @deprecated 
        * @description This view is deprecated and will eventually be removed in a future version (likely 3.0.0)
        */
        var DataCatalogView = Backbone.View.extend(
          /** @lends DataCatalogView.prototype */ {

            el: "#Content",

            isSubView: false,
            filters: true, // Turn on/off the filters in this view

            /**
            * If true, the view height will be adjusted to fit the height of the window
            * If false, the view height will be fixed via CSS
            * @type {Boolean}
            */
            fixedHeight: false,

            // The default global models for searching
            searchModel: null,
            searchResults: null,
            statsModel: null,
            mapModel: null,

            /**
            * The templates for this view
            * @type {Underscore.template}
            */
            template: _.template(CatalogTemplate),
            statsTemplate: _.template(CountTemplate),
            pagerTemplate: _.template(PagerTemplate),
            mainContentTemplate: _.template(MainContentTemplate),
            currentFilterTemplate: _.template(CurrentFilterTemplate),
            loadingTemplate: _.template(LoadingTemplate),
            metricStatTemplate: _.template("<span class='metric-icon'> <i class='icon" +
                " <%=metricIcon%>'></i> </span>" +
                "<span class='metric-value'> <i class='icon metric-icon'>" +
                "</i> </span>"),

            // Search mode
            mode: "map",

            // Map settings and storage
            map: null,
            ready: false,
            allowSearch: true,
            hasZoomed: false,
            hasDragged: false,
            markers: {},
            tiles: [],
            tileCounts: [],

            /**
             * The general error message to show as a title in the error box when there
             * is an error fetching results from solr
             * @type {string}
             * @default "Something went wrong while getting the list of datasets"
             * @since 2.15.0
             */
            solrErrorTitle: "Something went wrong while getting the list of datasets",

            /**
             * The user-friendly text to show when a solr request gives a status 500
             * error. If none is provided, then the error message that is returned from
             * solr will be displayed.
             * @type {string}
             * @since 2.15.0
             */
            solrError500Message: null,

            // Contains the geohashes for all the markers on the map (if turned on in the Map model)
            markerGeohashes: [],
            // Contains all the info windows for all the markers on the map (if turned on in the Map model)
            markerInfoWindows: [],
            // Contains all the info windows for each document in the search result list - to display on hover
            tileInfoWindows: [],
            // Contains all the currently visible markers on the map
            resultMarkers: [],
            // The geohash value for each tile drawn on the map
            tileGeohashes: [],
            mapFilterToggle: ".toggle-map-filter",

            // Delegated events for creating new items, and clearing completed ones.
            events: {
                "click #results_prev": "prevpage",
                "click #results_next": "nextpage",
                "click #results_prev_bottom": "prevpage",
                "click #results_next_bottom": "nextpage",
                "click .pagerLink": "navigateToPage",
                "click .filter.btn": "updateTextFilters",
                "keypress input[type='text'].filter": "triggerOnEnter",
                "focus input[type='text'].filter": "getAutocompletes",
                "change #sortOrder": "triggerSearch",
                "change #min_year": "updateYearRange",
                "change #max_year": "updateYearRange",
                "click #publish_year": "updateYearRange",
                "click #data_year": "updateYearRange",
                "click .remove-filter": "removeFilter",
                "click input[type='checkbox'].filter": "updateBooleanFilters",
                "click #clear-all": "resetFilters",
                "click .remove-addtl-criteria": "removeAdditionalCriteria",
                "click .collapse-me": "collapse",
                "click .filter-contain .expand-collapse-control": "toggleFilterCollapse",
                "click #toggle-map": "toggleMapMode",
                "click .toggle-map": "toggleMapMode",
                "click .toggle-list": "toggleList",
                "click .toggle-map-filter": "toggleMapFilter",
                "mouseover .open-marker": "showResultOnMap",
                "mouseout .open-marker": "hideResultOnMap",
                "mouseover .prevent-popover-runoff": "preventPopoverRunoff"
            },

            initialize: function(options) {
                var view = this;

                // Get all the options and apply them to this view
                if (options) {
                    var optionKeys = Object.keys(options);
                    _.each(optionKeys, function(key, i) {
                        view[key] = options[key];
                    });
                }
            },

            // Render the main view and/or re-render subviews. Don't call .html() here
            // so we don't lose state, rather use .setElement(). Delegate rendering
            // and event handling to sub views
                render: function () {

                // Use the global models if there are no other models specified at time of render
                if ((MetacatUI.appModel.get("searchHistory").length > 0) &&
                    (!this.searchModel || Object.keys(this.searchModel).length == 0)
                ) {
                  var lastSearchModels = _.last(MetacatUI.appModel.get("searchHistory"));

                  if(lastSearchModels){

                    if( lastSearchModels.search ){
                      this.searchModel = lastSearchModels.search.clone();
                    }

                    if( lastSearchModels.map ){
                      this.mapModel = lastSearchModels.map.clone();
                    }
                  }

                } else if ((typeof MetacatUI.appSearchModel !== "undefined") &&
                    (!this.searchModel || Object.keys(this.searchModel).length == 0)
                ) {
                    this.searchModel = MetacatUI.appSearchModel;
                    this.mapModel = MetacatUI.mapModel;
                    this.statsModel = MetacatUI.statsModel;
                }

                if (!this.mapModel && gmaps) {
                    this.mapModel = MetacatUI.mapModel;
                }

                if (((typeof this.searchResults === "undefined") ||
                    (!this.searchResults || Object.keys(this.searchResults).length == 0)) &&
                    (MetacatUI.appSearchResults && (Object.keys(MetacatUI.appSearchResults).length > 0))
                ) {
                    this.searchResults = MetacatUI.appSearchResults;

                    if( !this.statsModel ){
                      this.statsModel = MetacatUI.statsModel;
                    }

                    if( !this.mapModel ){
                      this.mapModel = MetacatUI.mapModel;
                    }
                }

                // Get the search mode - either "map" or "list"
                if ((typeof this.mode === "undefined") || !this.mode) {
                    this.mode = MetacatUI.appModel.get("searchMode");
                    if ((typeof this.mode === "undefined") || !this.mode) {
                        this.mode = "map";
                    }
                    MetacatUI.appModel.set("searchMode", this.mode);
                }
                if ($(window).outerWidth() <= 600) {
                    this.mode = "list";
                    MetacatUI.appModel.set("searchMode", "list");
                    gmaps = null;
                }

                if (!this.isSubView) {
                    MetacatUI.appModel.set("headerType", "default");
                    $("body").addClass("DataCatalog");
                } else {
                    this.$el.addClass("DataCatalog");
                }

                // Populate the search template with some model attributes
                var loadingHTML = this.loadingTemplate({
                    msg: "Retrieving member nodes..."
                });

                var templateVars = {
                    gmaps: gmaps,
                    mode: MetacatUI.appModel.get("searchMode"),
                    useMapBounds: this.searchModel.get("useGeohash"),
                    username: MetacatUI.appUserModel.get("username"),
                    isMySearch: (_.indexOf(this.searchModel.get("username"), MetacatUI.appUserModel.get("username")) > -1),
                    loading: loadingHTML,
                    searchModelRef: this.searchModel,
                    searchResultsRef: this.searchResults,
                    dataSourceTitle: (MetacatUI.theme == "dataone") ? "Member Node" : "Data source"
                }
                var cel = this.template(_.extend(this.searchModel.toJSON(), templateVars));

                this.$el.html(cel);

                //Hide the filters that are disabled in the AppModel settings
                _.each( this.$(".filter-contain[data-category]"), function(filterEl){
                  if( ! _.contains(MetacatUI.appModel.get("defaultSearchFilters"), $(filterEl).attr("data-category")) ){
                    $(filterEl).hide();
                  }
                }, this);

                // Store some references to key views that we use repeatedly
                this.$resultsview = this.$("#results-view");
                this.$results = this.$("#results");

                // Update stats
                this.updateStats();

                // Render the Google Map
                this.renderMap();

                // Initialize the tooltips
                var tooltips = $(".tooltip-this");

                // Find the tooltips that are on filter labels - add a slight delay to those
                var groupedTooltips = _.groupBy(tooltips, function(t) {
                    return ((($(t).prop("tagName") == "LABEL") ||
                        ($(t).parent().prop("tagName") == "LABEL")) &&
                        ($(t).parents(".filter-container").length > 0))
                });
                var forFilterLabel = true,
                    forOtherElements = false;

                $(groupedTooltips[forFilterLabel]).tooltip({
                    delay: {
                        show: "800"
                    }
                });
                $(groupedTooltips[forOtherElements]).tooltip();

                // Initialize all popover elements
                $(".popover-this").popover();

                // Initialize the resizeable content div
                $("#content").resizable({
                    handles: "n,s,e,w"
                });

                // Collapse the filters
                this.toggleFilterCollapse();

                // Iterate through each search model text attribute and show UI filter for each
                var categories = ["all", "attribute", "creator", "id", "taxon", "spatial",
                    "additionalCriteria", "annotation", "isPrivate"];
                var thisTerm = null;

                for (var i = 0; i < categories.length; i++) {
                    thisTerm = this.searchModel.get(categories[i]);

                    if (thisTerm === undefined || thisTerm === null) break;

                    for (var x = 0; x < thisTerm.length; x++) {
                        this.showFilter(categories[i], thisTerm[x]);
                    }
                }

                // List the Member Node filters
                var view = this;
                _.each(_.contains(MetacatUI.appModel.get("defaultSearchFilters"), "dataSource"), function(source, i) {
                    view.showFilter("dataSource", source);
                });

                // Add the custom query under the "Anything" filter
                if (this.searchModel.get("customQuery")) {
                    this.showFilter("all", this.searchModel.get("customQuery"));
                }

                // Register listeners; this is done here in render because the HTML
                // needs to be bound before the listenTo call can be made
                this.stopListening(this.searchResults);
                this.stopListening(this.searchModel);
                this.stopListening(MetacatUI.appModel);
                this.listenTo(this.searchResults, "reset", this.cacheSearch);
                this.listenTo(this.searchResults, "add", this.addOne);
                this.listenTo(this.searchResults, "reset", this.addAll);
                this.listenTo(this.searchResults, "reset", this.checkForProv);
                this.listenTo(this.searchResults, "error", this.showError);

                // List data sources
                this.listDataSources();
                this.listenTo(MetacatUI.nodeModel, "change:members", this.listDataSources);

                // listen to the MetacatUI.appModel for the search trigger
                this.listenTo(MetacatUI.appModel, "search", this.getResults);

                this.listenTo(MetacatUI.appUserModel, "change:loggedIn", this.triggerSearch);

                // and go to a certain page if we have it
                this.getResults();

                // Set a custom height on any elements that have the .auto-height class
                if ($(".auto-height").length > 0 && !this.fixedHeight) {
                    // Readjust the height whenever the window is resized
                    $(window).resize(this.setAutoHeight);
                    $(".auto-height-member").resize(this.setAutoHeight);
                }

                this.addAnnotationFilter();

                return this;
            },

            /**
             * addAnnotationFilter - Add the annotation filter to the view
             */
            addAnnotationFilter: function(){
              if (MetacatUI.appModel.get("bioportalAPIKey")) {
                var view = this;
                var popoverTriggerSelector = "[data-category='annotation'] .expand-collapse-control";
                if(!this.$el.find(popoverTriggerSelector)){
                  return
                }
                var annotationFilter = new AnnotationFilter({
                  popoverTriggerSelector: popoverTriggerSelector
                });
                this.$el
                  .find(popoverTriggerSelector)
                  .append(annotationFilter.el);
                annotationFilter.render();
                annotationFilter.off("annotationSelected");
                annotationFilter.on("annotationSelected", function(event, item){
                  $("#annotation_input").val(item.value);
                  view.updateTextFilters(event, item)
                });
              }
            },

            // Linked Data Object for appending the jsonld into the browser DOM
            getLinkedData: function() {
                // Find the MN info from the CN Node list
                var members = MetacatUI.nodeModel.get("members")
                for (var i = 0; i < members.length; i++) {
                    if (members[i].identifier == MetacatUI.nodeModel.get("currentMemberNode")) {
                        var nodeModelObject = members[i];
                    }
                }

                // JSON Linked Data Object
                let elJSON = {
                    "@context": {
                        "@vocab": "http://schema.org/"
                    },
                    "@type": "DataCatalog",
                };
                if (nodeModelObject) {
                    // "keywords": "",
                    // "provider": "",
                    let conditionalData = {
                        "description": nodeModelObject.description,
                        "identifier": nodeModelObject.identifier,
                        "image": nodeModelObject.logo,
                        "name": nodeModelObject.name,
                        "url": nodeModelObject.url
                    }
                    $.extend(elJSON, conditionalData)
                }


                // Check if the jsonld already exists from the previous data view
                // If not create a new script tag and append otherwise replace the text for the script
                if (!document.getElementById("jsonld")) {
                    var el = document.createElement("script");
                    el.type = "application/ld+json";
                    el.id = "jsonld";
                    el.text = JSON.stringify(elJSON);
                    document.querySelector("head").appendChild(el);
                } else {
                    var script = document.getElementById("jsonld");
                    script.text = JSON.stringify(elJSON);
                }
                return;
            },

            /*
             * Sets the height on elements in the main content area to fill up the entire area minus header and footer
             */
            setAutoHeight: function() {
                // If we are in list mode, don't determine the height of any elements because we are not "full screen"
                if (MetacatUI.appModel.get("searchMode") == "list" || this.fixedHeight) {
                    MetacatUI.appView.$(".auto-height").height("auto");
                    return;
                }

                // Get the heights of the header, navbar, and footer
                var otherHeight = 0;
                $(".auto-height-member").each(function(i, el) {
                    if ($(el).css("display") != "none") {
                        otherHeight += $(el).outerHeight(true);
                    }
                });

                // Get the remaining height left based on the window size
                var remainingHeight = $(window).outerHeight(true) - otherHeight;
                if (remainingHeight < 0) remainingHeight = $(window).outerHeight(true) || 300;
                else if (remainingHeight <= 120) remainingHeight = ($(window).outerHeight(true) - remainingHeight) || 300;

                // Adjust all elements with the .auto-height class
                $(".auto-height").height(remainingHeight);

                if (($("#map-container.auto-height").length > 0) && ($("#map-canvas").length > 0)) {
                    var otherHeight = 0;
                    $("#map-container.auto-height").children().each(function(i, el) {
                        if ($(el).attr("id") != "map-canvas") {
                            otherHeight += $(el).outerHeight(true);
                        }
                    });
                    var newMapHeight = remainingHeight - otherHeight;
                    if (newMapHeight > 100) {
                        $("#map-canvas").height(remainingHeight - otherHeight);
                    }
                }

                // Trigger a resize for the map so that all of the map background images are loaded
                if (gmaps && this.mapModel && this.mapModel.get("map")) {
                    google.maps.event.trigger(this.mapModel.get("map"), "resize");
                }
            },

            /*
             * ==================================================================================================
             *                                         PERFORMING SEARCH
             * ==================================================================================================
             */
            triggerSearch: function() {

                // Set the sort order
                var sortOrder = $("#sortOrder").val();
                if (sortOrder) {
                    this.searchModel.set("sortOrder", sortOrder);
                }

                // Trigger a search to load the results
                MetacatUI.appModel.trigger("search");

                if (!this.isSubView) {
                    // make sure the browser knows where we are
                    var route = Backbone.history.fragment;
                    if (route.indexOf("data") < 0) {
                        MetacatUI.uiRouter.navigate("data", { trigger: false, replace: true });
                    } else {
                        MetacatUI.uiRouter.navigate(route);
                    }
                }

                // ...but don't want to follow links
                return false;
            },

            triggerOnEnter: function(e) {
                if (e.keyCode != 13) return;

                // Update the filters
                this.updateTextFilters(e);
            },


            /**
             * getResults gets all the current search filters from the searchModel, creates a Solr query, and runs that query.
             * @param {number} page - The page of search results to get results for
             */
            getResults: function(page) {

                // Set the sort order based on user choice
                var sortOrder = this.searchModel.get("sortOrder");
                if (sortOrder) {
                    this.searchResults.setSort(sortOrder);
                }

                // Specify which fields to retrieve
                var fields = "";
                    fields += "id,";
                    fields += "seriesId,";
                    fields += "title,";
                    fields += "origin,";
                    fields += "pubDate,";
                    fields += "dateUploaded,";
                    fields += "abstract,";
                    fields += "resourceMap,";
                    fields += "beginDate,";
                    fields += "endDate,";
                    fields += "read_count_i,";
                    fields += "geohash_9,";
                    fields += "datasource,";
                    fields += "isPublic,";
                    fields += "documents,";
                    fields += "sem_annotation,";
                // Add spatial fields if the map is present
                if ( gmaps ) {
                    fields += "northBoundCoord,";
                    fields += "southBoundCoord,";
                    fields += "eastBoundCoord,";
                    fields += "westBoundCoord";
                }
                // Strip the last trailing comma if needed
                if ( fields[fields.length - 1] === "," ) {
                    fields = fields.substr(0, fields.length - 1);
                }
                this.searchResults.setfields(fields);

                // Get the query
                var query = this.searchModel.getQuery();

                // Specify which facets to retrieve
                if (gmaps && this.map) { // If we have Google Maps enabled
                    var geohashLevel = "geohash_" +
                        this.mapModel.determineGeohashLevel(this.map.zoom);
                    this.searchResults.facet.push(geohashLevel);
                }

                // Run the query
                this.searchResults.setQuery(query);

                // Get the page number
                if (this.isSubView) {
                    var page = 0;
                } else {
                    var page = MetacatUI.appModel.get("page");
                    if (page == null) {
                        page = 0;
                    }
                }
                this.searchResults.start = page * this.searchResults.rows;

                // Show or hide the reset filters button
                this.toggleClearButton();

                // go to the page
                this.showPage(page);

                // don't want to follow links
                return false;
            },

            /*
             * After the search results have been returned,
             * check if any of them are derived data or have derivations
             */
            checkForProv: function() {

                var maps = [],
                    hasSources = [],
                    hasDerivations = [],
                    mainSearchResults = this.searchResults;

                // Get a list of all the resource map IDs from the SolrResults collection
                maps = this.searchResults.pluck("resourceMap");
                maps = _.compact(_.flatten(maps));

                // Create a new Search model with a search that finds all members of these packages/resource maps
                var provSearchModel = new SearchModel({
                    formatType: [{
                        value: "DATA",
                        label: "data",
                        description: null
                    }],
                    exclude: [],
                    resourceMap: maps
                });

                // Create a new Solr Results model to store the results of this supplemental query
                var provSearchResults = new SearchResults(null, {
                    query: provSearchModel.getQuery(),
                    searchLogs: false,
                    usePOST: true,
                    rows: 150,
                    fields: provSearchModel.getProvFlList() + ",id,resourceMap"
                });

                // Trigger a search on that Solr Results model
                this.listenTo(provSearchResults, "reset", function(results) {
                    if (results.models.length == 0) return;

                    // See if any of the results have a value for a prov field
                    results.forEach(function(result) {
                        if ((!result.getSources().length) || (!result.getDerivations())) return;
                        _.each(result.get("resourceMap"), function(rMapID) {
                            if (_.contains(maps, rMapID)) {
                                var match = mainSearchResults.filter(function(mainSearchResult) {
                                    return _.contains(mainSearchResult.get("resourceMap"), rMapID)
                                });
                                if (match && match.length && (result.getSources().length > 0))
                                  hasSources.push(match[0].get("id"));
                                if (match && match.length && (result.getDerivations().length > 0))
                                  hasDerivations.push(match[0].get("id"));
                            }
                        });
                    });

                    // Filter out the duplicates
                    hasSources = _.uniq(hasSources);
                    hasDerivations = _.uniq(hasDerivations);

                    // If they do, find their corresponding result row here and add
                    // the prov icon (or just change the class to active)
                    _.each(hasSources, function(metadataID) {
                        var metadataDoc = mainSearchResults.findWhere({
                            id: metadataID
                        });
                        if (metadataDoc) {
                            metadataDoc.set("prov_hasSources", true);
                        }
                    });
                    _.each(hasDerivations, function(metadataID) {
                        var metadataDoc = mainSearchResults.findWhere({
                            id: metadataID
                        });
                        if (metadataDoc) {
                            metadataDoc.set("prov_hasDerivations", true);
                        }
                    });
                });
                provSearchResults.toPage(0);
            },

            cacheSearch: function() {
                MetacatUI.appModel.get("searchHistory").push({
                    search: this.searchModel.clone(),
                    map: this.mapModel ? this.mapModel.clone() : null
                });
                MetacatUI.appModel.trigger("change:searchHistory");
            },

            /*
             * ==================================================================================================
             *                                             FILTERS
             * ==================================================================================================
             */
            updateCheckboxFilter: function(e, category, value) {
                if (!this.filters) return;

                var checkbox = e.target;
                var checked = $(checkbox).prop("checked");

                if (typeof category == "undefined") var category = $(checkbox).attr("data-category");
                if (typeof value == "undefined") var value = $(checkbox).attr("value");

                // If the user just unchecked the box, then remove this filter
                if (!checked) {
                    this.searchModel.removeFromModel(category, value);
                    this.hideFilter(category, value);
                }
                // If the user just checked the box, then add this filter
                else {
                    var currentValue = this.searchModel.get(category);

                    // Get the description
                    var desc = $(checkbox).attr("data-description") || $(checkbox).attr("title");
                    if (typeof desc == "undefined" || !desc) desc = "";
                    // Get the label
                    var labl = $(checkbox).attr("data-label");
                    if (typeof labl == "undefined" || !labl) labl = "";

                    // Make the filter object
                    var filter = {
                        description: desc,
                        label: labl,
                        value: value
                    }

                    // If this filter category is an array, add this value to the array
                    if (Array.isArray(currentValue)) {
                        currentValue.push(filter);
                        this.searchModel.set(category, currentValue);
                        this.searchModel.trigger("change:" + category);
                    } else {
                        // If it isn't an array, then just update the model with a simple value
                        this.searchModel.set(category, filter);
                    }

                    // Show the filter element
                    this.showFilter(category, value, true, labl);

                    // Show the reset button
                    this.showClearButton();
                }

                // Route to page 1
                this.updatePageNumber(0);

                // Trigger a new search
                this.triggerSearch();
            },

            updateBooleanFilters: function(e) {
                if (!this.filters) return;

                // Get the category
                var checkbox = e.target;
                var category = $(checkbox).attr("data-category");
                var currentValue = this.searchModel.get(category);

                // If this filter is not enabled, exit this function
                if ( !_.contains(MetacatUI.appModel.get("defaultSearchFilters"), category) ){
                  return false;
                }

                //The year filter is handled in a different way
                if ((category == "pubYear") || (category == "dataYear")) return;

                // If the checkbox has a value, then update as a string value not boolean
                var value = $(checkbox).attr("value");
                if (value) {
                    this.updateCheckboxFilter(e, category, value);
                    return;
                } else value = $(checkbox).prop("checked");

                this.searchModel.set(category, value);

                // Add the filter to the UI
                if (value) {
                    this.showFilter(category, "", true);
                } else {
                // Remove the filter from the UI
                    value = "";
                    this.hideFilter(category, value);
                }

                // Show the reset button
                this.showClearButton();

                // Route to page 1
                this.updatePageNumber(0);

                // Trigger a new search
                this.triggerSearch();

                // Track this event
                MetacatUI.analytics?.trackEvent("search", "filter, " + category, value)
            },

            // Update the UI year slider and input values
            // Also update the model
            updateYearRange: function(e) {
                if (!this.filters) return;

                var viewRef = this,
                    userAction = !(typeof e === "undefined"),
                    model = this.searchModel,
                    pubYearChecked = $("#publish_year").prop("checked"),
                    dataYearChecked = $("#data_year").prop("checked");


                // If the year range slider has not been created yet
                if (!userAction && !$("#year-range").hasClass("ui-slider")) {

                    var defaultMin = typeof this.searchModel.defaults == "function" ? this.searchModel.defaults().yearMin : 1800,
                        defaultMax = typeof this.searchModel.defaults == "function" ? this.searchModel.defaults().yearMax : (new Date()).getUTCFullYear();

                    //jQueryUI slider
                    $("#year-range").slider({
                        range: true,
                        disabled: false,
                        min: defaultMin, //sets the minimum on the UI slider on initialization
                        max: defaultMax, //sets the maximum on the UI slider on initialization
                        values: [this.searchModel.get("yearMin"), this.searchModel.get("yearMax")], //where the left and right slider handles are
                        stop: function(event, ui) {

                            // When the slider is changed, update the input values
                            $("#min_year").val(ui.values[0]);
                            $("#max_year").val(ui.values[1]);

                            // Also update the search model
                            model.set("yearMin", ui.values[0]);
                            model.set("yearMax", ui.values[1]);

                            // If neither the publish year or data coverage year are checked
                            if (!$("#publish_year").prop("checked") && !$("#data_year").prop("checked")) {

                                // We want to check the data coverage year on the user's behalf
                                $("#data_year").prop("checked", "true");

                                // And update the search model
                                model.set("dataYear", true);
                            }

                            // Add the filter elements
                            if ($("#publish_year").prop("checked")) {
                                viewRef.showFilter($("#publish_year").attr("data-category"), true, false, ui.values[0] + " to " + ui.values[1], {
                                    replace: true
                                });
                            }
                            if ($("#data_year").prop("checked")) {
                                viewRef.showFilter($("#data_year").attr("data-category"), true, false, ui.values[0] + " to " + ui.values[1], {
                                    replace: true
                                });
                            }

                            // Route to page 1
                            viewRef.updatePageNumber(0);

                            // Trigger a new search
                            viewRef.triggerSearch();
                        }
                    });

                    // Get the minimum and maximum years of this current search and use those as the min and max values in the slider
                    this.statsModel.set("query", this.searchModel.getQuery());
                    this.listenTo(this.statsModel, "change:firstBeginDate", function() {
                        if (this.statsModel.get("firstBeginDate") == 0 || !this.statsModel.get("firstBeginDate")) {
                            $("#year-range").slider({
                                min: defaultMin
                            });
                            return;
                        }
                        var year = new Date(this.statsModel.get("firstBeginDate")).getUTCFullYear();
                        if (typeof year !== "undefined") {
                            $("#min_year").val(year);
                            $("#year-range").slider({
                                values: [year, $("#max_year").val()]
                            });

                            // If the slider min is still at the default value, then update with the min value found at this search
                            if ($("#year-range").slider("option", "min") == defaultMin) {
                                $("#year-range").slider({
                                    min: year
                                });
                            }

                            // Add the filter elements if this is set
                            if (viewRef.searchModel.get("pubYear")) {
                                viewRef.showFilter("pubYear", true, false, $("#min_year").val() + " to " + $("#max_year").val(), {
                                    replace: true
                                });
                            }
                            if (viewRef.searchModel.get("dataYear")) {
                                viewRef.showFilter("dataYear", true, false, $("#min_year").val() + " to " + $("#max_year").val(), {
                                    replace: true
                                });
                            }
                        }
                    });
                    // Only when the first begin date is retrieved, set the slider min and max values
                    this.listenTo(this.statsModel, "change:lastEndDate", function() {
                        if (this.statsModel.get("lastEndDate") == 0 || !this.statsModel.get("lastEndDate")) {
                            $("#year-range").slider({
                                max: defaultMax
                            });
                            return;
                        }
                        var year = new Date(this.statsModel.get("lastEndDate")).getUTCFullYear();
                        if (typeof year !== "undefined") {
                            $("#max_year").val(year);
                            $("#year-range").slider({
                                values: [$("#min_year").val(), year]
                            });

                            // If the slider max is still at the default value, then update with the max value found at this search
                            if ($("#year-range").slider("option", "max") == defaultMax) {
                                $("#year-range").slider({
                                    max: year
                                });
                            }

                            // Add the filter elements if this is set
                            if (viewRef.searchModel.get("pubYear")) {
                                viewRef.showFilter("pubYear", true, false, $("#min_year").val() + " to " + $("#max_year").val(), {
                                    replace: true
                                });
                            }
                            if (viewRef.searchModel.get("dataYear")) {
                                viewRef.showFilter("dataYear", true, false, $("#min_year").val() + " to " + $("#max_year").val(), {
                                    replace: true
                                });
                            }
                        }
                    });
                    this.statsModel.getFirstBeginDate();
                    this.statsModel.getLastEndDate();
                }
                // If the year slider has been created and the user initiated a new search using other filters
                else if (!userAction && (!this.searchModel.get("dataYear")) && (!this.searchModel.get("pubYear"))) {
                    // Reset the min and max year based on this search
                    this.statsModel.set("query", this.searchModel.getQuery());
                    this.statsModel.getFirstBeginDate();
                    this.statsModel.getLastEndDate();
                }
                // If either of the year type selectors is what brought us here, then determine whether the user
                // is completely removing both (reset both year filters) or just one (remove just that one filter)
                else if (userAction) {
                    // When both year types were unchecked, assume user wants to reset the year filter
                    if ((($(e.target).attr("id") == "data_year") || ($(e.target).attr("id") == "publish_year")) && (!pubYearChecked && !dataYearChecked)) {
                        // Reset the search model
                        this.searchModel.set("yearMin", defaultMin);
                        this.searchModel.set("yearMax", defaultMax);
                        this.searchModel.set("dataYear", false);
                        this.searchModel.set("pubYear", false);

                        // Reset the min and max year based on this search
                        this.statsModel.set("query", this.searchModel.getQuery());
                        this.statsModel.getFirstBeginDate();
                        this.statsModel.getLastEndDate();

                        // Slide the handles back to the defaults
                        $("#year-range").slider("values", [defaultMin, defaultMax]);

                        // Hide the filters
                        this.hideFilter("dataYear");
                        this.hideFilter("pubYear");
                    }
                    // If either of the year inputs have changed or if just one of the year types were unchecked
                    else {
                        var minVal = $("#min_year").val();
                        var maxVal = $("#max_year").val();

                        // Update the search model to match what is in the text inputs
                        this.searchModel.set("yearMin", minVal);
                        this.searchModel.set("yearMax", maxVal);
                        this.searchModel.set("dataYear", dataYearChecked);
                        this.searchModel.set("pubYear", pubYearChecked);

                        // If neither the publish year or data coverage year are checked
                        if (!pubYearChecked && !dataYearChecked) {

                            // We want to check the data coverage year on the user's behalf
                            $("#data_year").prop("checked", "true");

                            // And update the search model
                            model.set("dataYear", true);

                            // Add the filter elements
                            this.showFilter($("#data_year").attr("data-category"), true, true, minVal + " to " + maxVal, {
                                replace: true
                            });

                            // Track this event
                            MetacatUI.analytics?.trackEvent("search", "filter, Data Year", minVal + " to " + maxVal);

                        } else {
                            // Add the filter elements
                            if (pubYearChecked) {
                                this.showFilter($("#publish_year").attr("data-category"), true, true, minVal + " to " + maxVal, {
                                    replace: true
                                });

                                // Track this event
                                MetacatUI.analytics?.trackEvent("search", "filter, Publication Year", minVal + " to " + maxVal);

                            } else {
                                this.hideFilter($("#publish_year").attr("data-category"), true);
                            }

                            if (dataYearChecked) {
                                this.showFilter($("#data_year").attr("data-category"), true, true, minVal + " to " + maxVal, {
                                    replace: true
                                });

                                // Track this event
                                MetacatUI.analytics?.trackEvent("search", "filter, Data Year", minVal + " to " + maxVal);

                            } else {
                                this.hideFilter($("#data_year").attr("data-category"), true);
                            }
                        }
                    }

                    // Route to page 1
                    this.updatePageNumber(0);

                    // Trigger a new search
                    this.triggerSearch();
                }
            },

            updateTextFilters: function(e, item) {
                if (!this.filters) return;

                // Get the search/filter category
                var category = $(e.target).attr("data-category");

                // Try the parent elements if not found
                if (!category) {
                    var parents = $(e.target).parents().each(function() {
                        category = $(this).attr("data-category");
                        if (category) {
                            return false;
                        }
                    });
                }

                if (!category) {
                    return false;
                }

                // Get the input element
                var input = this.$el.find("#" + category + "_input");

                // Get the value of the associated input
                var term = (!item || !item.value) ? input.val() : item.value;
                var label = (!item || !item.filterLabel) ? null : item.filterLabel;
                var filterDesc = (!item || !item.desc) ? null : item.desc;

                // Check that something was actually entered
                if ((term == "") || (term == " ")) {
                    return false;
                }

                // Close the autocomplete box
                if (e.type == "hoverautocompleteselect") {
                    $(input).hoverAutocomplete("close");
                } else if ($(input).data("ui-autocomplete") != undefined) {
                    // If the autocomplete has been initialized, then close it
                    $(input).autocomplete("close");
                }

                // Get the current searchModel array for this category
                var filtersArray = _.clone(this.searchModel.get(category));

                if (typeof filtersArray == "undefined") {
                    console.error("The filter category '" + category + "' does not exist in the Search model. Not sending this search term.");
                    return false;
                }

                // Check if this entry is a duplicate
                var duplicate = (function() {
                    for (var i = 0; i < filtersArray.length; i++) {
                        if (filtersArray[i].value === term) {
                            return true;
                        }
                    }
                })();

                if (duplicate) {
                    // Display a quick message
                    if ($("#duplicate-" + category + "-alert").length <= 0) {
                        $("#current-" + category + "-filters").prepend(
                            "<div class='alert alert-block' id='duplicate-' + category + '-alert'>" +
                            "You are already using that filter" +
                            "</div>"
                        );

                        $("#duplicate-" + category + "-alert").delay(2000).fadeOut(500, function() {
                            this.remove();
                        });
                    }

                    return false;
                }

                // Add the new entry to the array of current filters
                var filter = {
                    value: term,
                    filterLabel: label,
                    label: label,
                    description: filterDesc
                };
                filtersArray.push(filter);

                // Replace the current array with the new one in the search model
                this.searchModel.set(category, filtersArray);

                // Show the UI filter
                this.showFilter(category, filter, false, label);

                // Clear the input
                input.val("");

                // Route to page 1
                this.updatePageNumber(0);

                // Trigger a new search
                this.triggerSearch();

                // Track this event
                MetacatUI.analytics?.trackEvent("search", "filter, " + category, term);

            },

            // Removes a specific filter term from the searchModel
            removeFilter: function(e) {
                // Get the parent element that stores the filter term
                var filterNode = $(e.target).parent();

                // Find this filter's category and value
                var category = filterNode.attr("data-category") || filterNode.parent().attr("data-category"),
                    value = $(filterNode).attr("data-term");

                // Remove this filter from the searchModel
                this.searchModel.removeFromModel(category, value);

                // Hide the filter from the UI
                this.hideFilter(category, value);

                // If there is an associated checkbox with this filter, uncheck it
                var assocCheckbox,
                    checkboxes = this.$("input[type='checkbox'][data-category='" + category + "']");

                //If there are more than one checkboxes in this category, match by value
                if (checkboxes.length > 1) {
                    assocCheckbox = _.find(checkboxes, function(checkbox){
                      return $(checkbox).val() == value;
                    });
                }
                //If there is only one checkbox in this category, default to it
                else if( checkboxes.length == 1 ){
                  assocCheckbox = checkboxes[0];
                }

                //If there is an associated checkbox, uncheck it
                if (assocCheckbox) {
                  //Uncheck it
                  $(assocCheckbox).prop("checked", false);
                }

                // Route to page 1
                this.updatePageNumber(0);

                // Trigger a new search
                this.triggerSearch();

            },

            // Clear all the currently applied filters
            resetFilters: function() {
                var viewRef = this;

                this.allowSearch = true;

                // Hide all the filters in the UI
                $.each(this.$(".current-filter"), function() {
                    viewRef.hideEl(this);
                });

                // Hide the clear button
                this.hideClearButton();

                // Then reset the model
                this.searchModel.clear();

                //Reset the map model
                if(this.mapModel){
                  this.mapModel.clear();
                }

                // Reset the year slider handles
                $("#year-range").slider("values", [this.searchModel.get("yearMin"), this.searchModel.get("yearMax")])
                //and the year inputs
                $("#min_year").val(this.searchModel.get("yearMin"));
                $("#max_year").val(this.searchModel.get("yearMax"));

                // Reset the checkboxes
                $("#includes_data").prop("checked", this.searchModel.get("documents"));
                $("#data_year").prop("checked", this.searchModel.get("dataYear"));
                $("#publish_year").prop("checked", this.searchModel.get("pubYear"));
                $("#is_private_data").prop("checked", this.searchModel.get("isPrivate"));
                this.listDataSources();

                // Zoom out the Google Map
                this.resetMap();
                this.renderMap();

                // Route to page 1
                this.updatePageNumber(0);

                // Trigger a new search
                this.triggerSearch();
            },

            hideEl: function(element) {
                // Fade out and remove the element
                $(element).fadeOut("slow", function() {
                    $(element).remove();
                });
            },

            // Removes a specified filter node from the DOM
            hideFilter: function(category, value) {
                if (!this.filters) return;

                if (typeof value === "undefined") {
                    var filterNode = this.$(".current-filters[data-category='" +
                        category + "']").children(".current-filter");
                } else {
                    var filterNode = this.$(".current-filters[data-category='" +
                        category + "']").children("[data-term='" + value + "']");
                }

                // Try finding it a different way
                if (!filterNode || !filterNode.length) {
                    filterNode = this.$(".current-filter[data-category='" + category + "']");
                }

                // Remove the filter node from the DOM
                this.hideEl(filterNode);
            },

            // Adds a specified filter node to the DOM
            showFilter: function(category, term, checkForDuplicates, label, options) {
                if (!this.filters) return;

                var viewRef = this;

                if (typeof term === "undefined") return false;

                // Get the element to add the UI filter node to
                // The pattern is #current-<category>-filters
                var filterContainer = this.$el.find("#current-" + category + "-filters");

                // Allow the option to only display this exact filter category and term once to the DOM
                // Helpful when adding a filter that is not stored in the search model (for display only)
                if (checkForDuplicates) {
                    var duplicate = false;

                    // Get the current terms from the DOM and check against the new term
                    filterContainer.children().each(function() {
                        if ($(this).attr("data-term") == term) {
                            duplicate = true;
                        }
                    });

                    // If there is a duplicate, exit without adding it
                    if (duplicate) {
                        return;
                    }
                }

                var value = null,
                    desc = null;

                // See if this filter is an object and extract the filter attributes
                if (typeof term === "object") {
                    if (typeof term.description !== "undefined") {
                        desc = term.description;
                    }
                    if (typeof term.filterLabel !== "undefined") {
                        label = term.filterLabel;
                    } else if ((typeof term.label !== "undefined") && (term.label)) {
                        label = term.label;
                    } else {
                        label = null;
                    }
                    if (typeof term.value !== "undefined") {
                        value = term.value;
                    }
                } else {
                    value = term;

                    // Find the filter label
                    if ((typeof label === "undefined") || !label) {

                        // Use the filter value for the label, sans any leading # character
                        if (value.indexOf("#") > 0) {
                            label = value.substring(value.indexOf("#"));
                        }
                    }

                    desc = label;
                }

                var categoryLabel = this.searchModel.fieldLabels[category];
                if ((typeof categoryLabel === "undefined") && (category == "additionalCriteria")) categoryLabel = "";
                if (typeof categoryLabel === "undefined") categoryLabel = category;

                // Add a filter node to the DOM
                var filterEl = viewRef.currentFilterTemplate({
                    category: Utilities.encodeHTML(categoryLabel),
                    value: Utilities.encodeHTML(value),
                    label: Utilities.encodeHTML(label),
                    description: Utilities.encodeHTML(desc)
                });

                // Add the filter to the page - either replace or tack on
                if (options && options.replace) {
                    var currentFilter = filterContainer.find(".current-filter");
                    if (currentFilter.length > 0) {
                        currentFilter.replaceWith(filterEl);
                    } else {
                        filterContainer.prepend(filterEl);
                    }
                } else {
                    filterContainer.prepend(filterEl);
                }

                // Tooltips and Popovers
                $(filterEl).tooltip({
                    delay: {
                        show: 800
                    }
                });

                return;
            },

            /*
             * Get the member node list from the model and list the members in the filter list
             */
            listDataSources: function() {
                if (!this.filters) return;

                if (MetacatUI.nodeModel.get("members").length < 1) return;

                // Get the member nodes
                var members = _.sortBy(MetacatUI.nodeModel.get("members"), function(m) {
                    if (m.name) {
                        return m.name.toLowerCase();
                    } else {
                        return "";
                    }
                });
                var filteredMembers = _.reject(members, function(m) {
                    return m.status != "operational"
                });

                // Get the current search filters for data source
                var currentFilters = this.searchModel.get("dataSource");

                // Create an HTML list
                var listMax = 4,
                    numHidden = filteredMembers.length - listMax,
                    list = $(document.createElement("ul")).addClass("checkbox-list");

                // Add a checkbox and label for each member node in the node model
                _.each(filteredMembers, function(member, i) {
                    var listItem = document.createElement("li"),
                        input = document.createElement("input"),
                        label = document.createElement("label");

                    // If this member node is already a data source filter, then the checkbox is checked
                    var checked = _.findWhere(currentFilters, {
                        value: member.identifier
                    }) ? true : false;

                    // Create a textual label for this data source
                    $(label).addClass("ellipsis")
                        .attr("for", member.identifier)
                        .html(member.name);

                    // Create a checkbox for this data source
                    $(input).addClass("filter")
                        .attr("type", "checkbox")
                        .attr("data-category", "dataSource")
                        .attr("id", member.identifier)
                        .attr("name", "dataSource")
                        .attr("value", member.identifier)
                        .attr("data-label", member.name)
                        .attr("data-description", member.description);

                    // Add tooltips to the label element
                    $(label).tooltip({
                        placement: "top",
                        delay: {
                            "show": 900
                        },
                        trigger: "hover",
                        viewport: "#sidebar",
                        title: member.description
                    });

                    // If this data source is already selected as a filter (from the search model), then check the checkbox
                    if (checked) $(input).prop("checked", "checked");

                    // Collapse some of the checkboxes and labels after a certain amount
                    if (i > (listMax - 1)) {
                        $(listItem).addClass("hidden");
                    }

                    // Insert a "More" link after a certain amount to enable users to expand the list
                    if (i == listMax) {
                        var moreLink = document.createElement("a");
                        $(moreLink).html("Show " + numHidden + " more")
                            .addClass("more-link pointer toggle-list")
                            .append($(document.createElement("i")).addClass("icon icon-expand-alt"));
                        $(list).append(moreLink);
                    }

                    // Add this checkbox and laebl to the list
                    $(listItem).append(input).append(label);
                    $(list).append(listItem);
                });

                if (numHidden > 0) {
                    var lessLink = document.createElement("a");
                    $(lessLink).html("Collapse member nodes")
                        .addClass("less-link toggle-list pointer hidden")
                        .append($(document.createElement("i")).addClass("icon icon-collapse-alt"));

                    $(list).append(lessLink);
                }

                // Add the list of checkboxes to the placeholder
                var container = $(".member-nodes-placeholder");
                $(container).html(list);
                $(".tooltip-this").tooltip();
            },

            resetDataSourceList: function() {
                if (!this.filters) return;

                // Reset the Member Nodes checkboxes
                var mnFilterContainer = $("#member-nodes-container"),
                    defaultMNs = this.searchModel.get("dataSource");

                // Make sure the member node filter exists
                if (!mnFilterContainer || mnFilterContainer.length == 0) return false;
                if ((typeof defaultMNs === "undefined") || !defaultMNs) return false;

                // Reset each member node checkbox
                var boxes = $(mnFilterContainer).find(".filter").prop("checked", false);

                // Check the member node checkboxes that are defaults in the search model
                _.each(defaultMNs, function(member, i) {
                    var value = null;

                    // Allow for string search model filter values and object filter values
                    if ((typeof member !== "object") && member) value = member;
                    else if ((typeof member.value === "undefined") || !member.value) value = "";
                    else value = member.value;

                    $(mnFilterContainer).find("checkbox[value='" + value + "']").prop("checked", true);
                });

                return true;
            },

            toggleList: function(e) {
                if (!this.filters) return;

                var link = e.target,
                    controls = $(link).parents("ul").find(".toggle-list"),
                    list = $(link).parents("ul"),
                    isHidden = !(list.find(".more-link").is(".hidden"));

                // Hide/Show the list
                if (isHidden) {
                    list.children("li").slideDown();
                } else {
                    list.children("li.hidden").slideUp();
                }

                // Hide/Show the control links
                controls.toggleClass("hidden");
            },


            // add additional criteria to the search model based on link click
            additionalCriteria: function(e) {
                // Get the clicked node
                var targetNode = $(e.target);

                // If this additional criteria is already applied, remove it
                if (targetNode.hasClass("active")) {
                    this.removeAdditionalCriteria(e);
                    return false;
                }

                // Get the filter criteria
                var term = targetNode.attr("data-term");

                // Find this element's category in the data-category attribute
                var category = targetNode.attr("data-category");

                // style the selection
                $(".keyword-search-link").removeClass("active");
                $(".keyword-search-link").parent().removeClass("active");
                targetNode.addClass("active");
                targetNode.parent().addClass("active");

                // Add this criteria to the search model
                this.searchModel.set(category, [term]);

                // Trigger the search
                this.triggerSearch();

                // prevent default action of click
                return false;

            },

            removeAdditionalCriteria: function(e) {

                // Get the clicked node
                var targetNode = $(e.target);

                // Reference to model
                var model = this.searchModel;

                // remove the styling
                $(".keyword-search-link").removeClass("active");
                $(".keyword-search-link").parent().removeClass("active");

                // Get the term
                var term = targetNode.attr("data-term");

                // Get the current search model additional criteria
                var current = this.searchModel.get("additionalCriteria");
                // If this term is in the current search model (should be)...
                if (_.contains(current, term)) {
                    //then remove it
                    var newTerms = _.without(current, term);
                    model.set("additionalCriteria", newTerms);
                }

                // Route to page 1
                this.updatePageNumber(0);

                // Trigger a new search
                this.triggerSearch();
            },

            // Get the facet counts
            getAutocompletes: function(e) {
                if (!e) return;

                // Get the text input to determine the filter type
                var input = $(e.target),
                    category = input.attr("data-category");

                if (!this.filters || !category) return;

                var viewRef = this;

                // Create the facet query by using our current search query
                var facetQuery = "q=" + this.searchResults.currentquery +
                    "&rows=0" +
                    this.searchModel.getFacetQuery(category) +
                    "&wt=json&";

                // If we've cached these filter results, then use the cache instead of sending a new request
                if (!MetacatUI.appSearchModel.autocompleteCache) MetacatUI.appSearchModel.autocompleteCache = {};
                else if (MetacatUI.appSearchModel.autocompleteCache[facetQuery]) {
                    this.setupAutocomplete(input, MetacatUI.appSearchModel.autocompleteCache[facetQuery]);
                    return;
                }

                // Get the facet counts for the autocomplete
                var requestSettings = {
                    url: MetacatUI.appModel.get("queryServiceUrl") + facetQuery,
                    type: "GET",
                    dataType: "json",
                    success: function(data, textStatus, xhr) {

                        var suggestions = [],
                            facetLimit = 999;

                        // Get all the facet counts
                        _.each(category.split(","), function(c) {
                            if (typeof c == "string") c = [c];
                            _.each(c, function(thisCategory) {
                                // Get the field name(s)
                                var fieldNames = MetacatUI.appSearchModel.facetNameMap[thisCategory];
                                if (typeof fieldNames == "string") fieldNames = [fieldNames];

                                // Get the facet counts
                                _.each(fieldNames, function(fieldName) {
                                    suggestions.push(data.facet_counts.facet_fields[fieldName]);
                                });
                            });
                        });
                        suggestions = _.flatten(suggestions);

                        // Format the suggestions
                        var rankedSuggestions = new Array();
                        for (var i = 0; i < Math.min(suggestions.length - 1, facetLimit); i += 2) {

                                      //The label is the item value
                                      var label = suggestions[i];

                                      //For all categories except the 'all' category, display the facet count
                                      if(category != "all"){
                                        label += " (" + suggestions[i+1] + ")";
                                      }

                                      //Push the autocomplete item to the array
                                      rankedSuggestions.push({
                                        value: suggestions[i],
                                        label: label
                                      });
                        }

                        // Save these facets in the app so we don't have to send another query
                        MetacatUI.appSearchModel.autocompleteCache[facetQuery] = rankedSuggestions;

                        // Now setup the actual autocomplete menu
                        viewRef.setupAutocomplete(input, rankedSuggestions);
                    }
                }
                $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
            },

            setupAutocomplete: function(input, rankedSuggestions) {
                var viewRef = this;

                //Override the _renderItem() function which renders a single autocomplete item.
                // We want to use the 'title' HTML attribute on each item.
                // This method must create a new <li> element, append it to the menu, and return it.
                $.widget( "custom.autocomplete", $.ui.autocomplete, {
                  _renderItem: function(ul, item) {
                    return $( document.createElement("li") )
                            .attr( "title", item.label )
                            .append( item.label )
                            .appendTo( ul );
                  }
                });
                input.autocomplete({
                    source: function(request, response) {
                        var term = $.ui.autocomplete.escapeRegex(request.term),
                            startsWithMatcher = new RegExp("^" + term, "i"),
                            startsWith = $.grep(rankedSuggestions, function(value) {
                                return startsWithMatcher.test(value.label || value.value || value);
                            }),
                            containsMatcher = new RegExp(term, "i"),
                            contains = $.grep(rankedSuggestions, function(value) {
                                return $.inArray(value, startsWith) < 0 &&
                                    containsMatcher.test(value.label || value.value || value);
                            });

                        response(startsWith.concat(contains));
                    },
                    select: function(event, ui) {
                        // set the text field
                        input.val(ui.item.value);
                        // add to the filter immediately
                        viewRef.updateTextFilters(event, ui.item);
                        // prevent default action
                        return false;
                    },
                    position: {
                        my: "left top",
                        at: "left bottom",
                        collision: "flipfit"
                    }
                });
            },

            hideClearButton: function() {
                if (!this.filters) return;

                // Hide the current filters panel
                this.$(".current-filters-container").slideUp();

                // Hide the reset button
                $("#clear-all").addClass("hidden");
                this.setAutoHeight();
            },

            showClearButton: function() {
                if (!this.filters) return;

                // Show the current filters panel
                if (_.difference(this.searchModel.getCurrentFilters(), this.searchModel.spatialFilters).length > 0) {
                    this.$(".current-filters-container").slideDown();
                }

                // Show the reset button
                $("#clear-all").removeClass("hidden");
                this.setAutoHeight();
            },

            /*
             * ==================================================================================================
             *                                             NAVIGATING THE UI
             * ==================================================================================================
             */
            // Update all the statistics throughout the page
            updateStats: function() {
                if (this.searchResults.header != null) {
                    this.$statcounts = this.$("#statcounts");
                    this.$statcounts.html(
                        this.statsTemplate({
                            start: this.searchResults.header.get("start") + 1,
                            end: this.searchResults.header.get("start") + this.searchResults.length,
                            numFound: this.searchResults.header.get("numFound")
                        })
                    );
                }

                // piggy back here
                this.updatePager();
            },

            updatePager: function() {
                if (this.searchResults.header != null) {
                    var pageCount = Math.ceil(this.searchResults.header.get("numFound") / this.searchResults.header.get("rows"));

                    // If no results were found, display a message instead of the list and clear the pagination.
                    if (pageCount == 0) {
                        this.$results.html("<p id='no-results-found'>No results found.</p>");

                        this.$("#resultspager").html("");
                        this.$(".resultspager").html("");
                    }
                    // Do not display the pagination if there is only one page
                    else if (pageCount == 1) {
                        this.$("#resultspager").html("");
                        this.$(".resultspager").html("");
                    } else {
                        var pages = new Array(pageCount);

                        // mark current page correctly, avoid NaN
                        var currentPage = -1;
                        try {
                            currentPage = Math.floor((this.searchResults.header.get("start") / this.searchResults.header.get("numFound")) * pageCount);
                        } catch (ex) {
                            console.log("Exception when calculating pages:" + ex.message);
                        }

                        // Populate the pagination element in the UI
                        this.$(".resultspager").html(
                            this.pagerTemplate({
                                pages: pages,
                                currentPage: currentPage
                            })
                        );
                        this.$("#resultspager").html(
                            this.pagerTemplate({
                                pages: pages,
                                currentPage: currentPage
                            })
                        );
                    }
                }
            },

            updatePageNumber: function(page) {
                MetacatUI.appModel.set("page", page);

                if (!this.isSubView) {
                    var route = Backbone.history.fragment,
                        subroutePos = route.indexOf("/page/"),
                        newPage = parseInt(page) + 1;

                    //replace the last number with the new one
                    if ((page > 0) && (subroutePos > -1)) {
                        route = route.replace(/\d+$/, newPage);
                    } else if (page > 0) {
                        route += "/page/" + newPage;
                    } else if (subroutePos >= 0) {
                        route = route.substring(0, subroutePos);
                    }

                    MetacatUI.uiRouter.navigate(route);
                }
            },

            // Next page of results
            nextpage: function() {
                this.loading();
                this.searchResults.nextpage();
                this.$resultsview.show();
                this.updateStats();

                var page = MetacatUI.appModel.get("page");
                page++;
                this.updatePageNumber(page);
            },

            // Previous page of results
            prevpage: function() {
                this.loading();
                this.searchResults.prevpage();
                this.$resultsview.show();
                this.updateStats();

                var page = MetacatUI.appModel.get("page");
                page--;
                this.updatePageNumber(page);
            },

            navigateToPage: function(event) {
                var page = $(event.target).attr("page");
                this.showPage(page);
            },

            showPage: function(page) {
                this.loading();
                this.searchResults.toPage(page);
                this.$resultsview.show();
                this.updateStats();
                this.updatePageNumber(page);
                this.updateYearRange();
            },

            /*
             * ==================================================================================================
             *                                             THE MAP
             * ==================================================================================================
             */
                renderMap: function () {

                    // If gmaps isn't enabled or loaded with an error, use list mode
                    if (!gmaps || this.mode == "list") {
                        this.ready = true;
                        this.mode = "list";
                        return;
                    }

                    if (this.isSubView) {
                        this.$el.addClass("mapMode");
                    } else {
                        $("body").addClass("mapMode");
                    }

                // Get the map options and create the map
                gmaps.visualRefresh = true;
                var mapOptions = this.mapModel.get("mapOptions");
                var defaultZoom = mapOptions.zoom;
                $("#map-container").append("<div id='map-canvas'></div>");
                this.map = new gmaps.Map($("#map-canvas")[0], mapOptions);
                this.mapModel.set("map", this.map);
                this.hasZoomed  = false;
                this.hasDragged = false;

                // Hide the map filter toggle element
                this.$(this.mapFilterToggle).hide();

                // Store references
                var mapRef = this.map;
                var viewRef = this;

                google.maps.event.addListener(mapRef, "zoom_changed", function() {
                  // If the map is zoomed in further than the default zoom level,
                  // than we want to mark the map as zoomed in
                  if(viewRef.map.getZoom() > defaultZoom){
                    viewRef.hasZoomed = true;
                  }
                  //If we are at the default zoom level or higher, than do not mark the map
                  // as zoomed in
                  else{
                    viewRef.hasZoomed = false;
                  }
                });

                google.maps.event.addListener(mapRef, "dragend", function() {
                  viewRef.hasDragged = true;
                });

                google.maps.event.addListener(mapRef, "idle", function(){
                  // Remove all markers from the map
                  for (var i = 0; i < viewRef.resultMarkers.length; i++) {
                      viewRef.resultMarkers[i].setMap(null);
                  }
                  viewRef.resultMarkers = new Array();

                  //Check if the user has interacted with the map just now, and if so, we
                  // want to alter the geohash filter (changing the geohash values or resetting it completely)
                  var alterGeohashFilter = viewRef.allowSearch || viewRef.hasZoomed || viewRef.hasDragged;
                  if( !alterGeohashFilter ){
                    return;
                  }

                  //Determine if the map needs to be recentered. The map only needs to be
                  // recentered if it is not at the default lat,long center point AND it
                  // is not zoomed in or dragged to a new center point
                  var setGeohashFilter = viewRef.hasZoomed && viewRef.isMapFilterEnabled();

                  //If we are using the geohash filter defined by this map, then
                  // apply the filter and trigger a new search
                  if( setGeohashFilter ){

                    viewRef.$(viewRef.mapFilterToggle).show();

                    // Get the Google map bounding box
                    var boundingBox = mapRef.getBounds();

                    // Set the search model spatial filters
                    // Encode the Google Map bounding box into geohash
                    var north = boundingBox.getNorthEast().lat(),
                        west = boundingBox.getSouthWest().lng(),
                        south = boundingBox.getSouthWest().lat(),
                        east = boundingBox.getNorthEast().lng();

                    viewRef.searchModel.set("north", north);
                    viewRef.searchModel.set("west", west);
                    viewRef.searchModel.set("south", south);
                    viewRef.searchModel.set("east", east);

                    // Save the center position and zoom level of the map
                    viewRef.mapModel.get("mapOptions").center = mapRef.getCenter();
                    viewRef.mapModel.get("mapOptions").zoom = mapRef.getZoom();

                    // Determine the precision of geohashes to search for
                    var zoom = mapRef.getZoom();

                    var precision = viewRef.mapModel.getSearchPrecision(zoom);

                    // Get all the geohash tiles contained in the map bounds
                    var geohashBBoxes = nGeohash.bboxes(south, west, north, east, precision);

                    // Save our geohash search settings
                    viewRef.searchModel.set("geohashes", geohashBBoxes);
                    viewRef.searchModel.set("geohashLevel", precision);

                    //Start back at page 0
                    MetacatUI.appModel.set("page", 0);

                    //Mark the view as ready to start a search
                    viewRef.ready = true;

                    // Trigger a new search
                    viewRef.triggerSearch();

                    viewRef.allowSearch = false;
                  }
                  else{

                    //Reset the map filter
                    viewRef.resetMap();

                    //Start back at page 0
                    MetacatUI.appModel.set("page", 0);

                    //Mark the view as ready to start a search
                    viewRef.ready = true;

                    // Trigger a new search
                    viewRef.triggerSearch();

                    viewRef.allowSearch = false;

                    return;
                  }
                });

            },

            // Resets the model and view settings related to the map
            resetMap: function() {
                if (!gmaps) {
                    return;
                }

                // First reset the model
                // The categories pertaining to the map
                var categories = ["east", "west", "north", "south"];

                // Loop through each and remove the filters from the model
                for (var i = 0; i < categories.length; i++) {
                    this.searchModel.set(categories[i], null);
                }

                // Reset the map settings
                this.searchModel.resetGeohash();
                this.mapModel.set("mapOptions", this.mapModel.defaults().mapOptions);

                this.allowSearch = false;
            },

            isMapFilterEnabled: function(){
              var toggleInput = this.$("input" + this.mapFilterToggle);
              if ((typeof toggleInput === "undefined") || !toggleInput) return;

              return $(toggleInput).prop("checked");
            },

            toggleMapFilter: function(e, a) {
                var toggleInput = this.$("input" + this.mapFilterToggle);
                if ((typeof toggleInput === "undefined") || !toggleInput) return;

                var isOn = $(toggleInput).prop("checked");

                // If the user clicked on the label, then change the checkbox for them
                if (e.target.tagName != "INPUT") {
                    isOn = !isOn;
                    toggleInput.prop("checked", isOn);
                }

                google.maps.event.trigger(this.mapModel.get("map"), "idle");

                // Track this event
                MetacatUI.analytics?.trackEvent("map", (isOn ? "on" : "off"));

            },

            /**
             * Show the marker, infoWindow, and bounding coordinates polygon on
             the map when the user hovers on the marker icon in the result list
             * @param {Event} e
             */
            showResultOnMap: function(e) {
                // Exit if maps are not in use
                if ((this.mode != "map") || (!gmaps)) {
                    return false;
                }

                // Get the attributes about this dataset
                var resultRow = e.target,
                    id = $(resultRow).attr("data-id");
                // The mouseover event might be triggered by a nested element, so loop through the parents to find the id
                if (typeof id == "undefined") {
                    $(resultRow).parents().each(function() {
                        if (typeof $(this).attr("data-id") != "undefined") {
                            id = $(this).attr("data-id");
                            resultRow = this;
                        }
                    });
                }

                // Find the tile for this data set and highlight it on the map
                var resultGeohashes = this.searchResults.findWhere({
                    id: id
                }).get("geohash_9");
                for (var i = 0; i < resultGeohashes.length; i++) {
                    var thisGeohash = resultGeohashes[i],
                        latLong = nGeohash.decode(thisGeohash),
                        position = new google.maps.LatLng(latLong.latitude, latLong.longitude),
                        containingTileGeohash = _.find(this.tileGeohashes, function(g) {
                            return thisGeohash.indexOf(g) == 0
                        }),
                        containingTile = _.findWhere(this.tiles, {
                            geohash: containingTileGeohash
                        });

                    // If this is a geohash for a georegion outside the map, do not highlight a tile or display a marker
                    if (typeof containingTile === "undefined") continue;

                    this.highlightTile(containingTile);

                    // Set up the options for each marker
                    var markerOptions = {
                        position: position,
                        icon: this.mapModel.get("markerImage"),
                        zIndex: 99999,
                        map: this.map
                    };

                    // Create the marker and add to the map
                    var marker = new google.maps.Marker(markerOptions);

                    this.resultMarkers.push(marker);

                }
            },

            /**
             * Hide the marker, infoWindow, and bounding coordinates polygon on
             the map when the user stops hovering on the marker icon in the result list
             * @param {Event} e - The event that brought us to this function
             */
            hideResultOnMap: function(e) {
                // Exit if maps are not in use
                if ((this.mode != "map") || (!gmaps)) {
                    return false;
                }

                // Get the attributes about this dataset
                var resultRow = e.target,
                    id = $(resultRow).attr("data-id");
                // The mouseover event might be triggered by a nested element, so loop through the parents to find the id
                if (typeof id == "undefined") {
                    $(e.target).parents().each(function() {
                        if (typeof $(this).attr("data-id") != "undefined") {
                            id = $(this).attr("data-id");
                            resultRow = this;
                        }
                    });
                }

                // Get the map tile for this result and un-highlight it
                var resultGeohashes = this.searchResults.findWhere({
                    id: id
                }).get("geohash_9");
                for (var i = 0; i < resultGeohashes.length; i++) {
                    var thisGeohash = resultGeohashes[i],
                        containingTileGeohash = _.find(this.tileGeohashes, function(g) {
                            return thisGeohash.indexOf(g) == 0
                        }),
                        containingTile = _.findWhere(this.tiles, {
                            geohash: containingTileGeohash
                        });

                    // If this is a geohash for a georegion outside the map, do not unhighlight a tile
                    if (typeof containingTile === "undefined") continue;

                    // Unhighlight the tile
                    this.unhighlightTile(containingTile);
                }

                // Remove all markers from the map
                _.each(this.resultMarkers, function(marker) {
                    marker.setMap(null);
                });
                this.resultMarkers = new Array();
            },

            /**
             * Create a tile for each geohash facet. A separate tile label is added to the map with the count of the facet.
             **/
            drawTiles: function () {

                // Exit if maps are not in use
                if ((this.mode != "map") || (!gmaps)) {
                    return false;
                }

                TextOverlay.prototype = new google.maps.OverlayView();

                function TextOverlay(options) {
                    // Now initialize all properties.
                    this.bounds_ = options.bounds;
                    this.map_ = options.map;
                    this.text = options.text;
                    this.color = options.color;

                    var length = options.text.toString().length;
                    if (length == 1) this.width = 8;
                    else if (length == 2) this.width = 17;
                    else if (length == 3) this.width = 25;
                    else if (length == 4) this.width = 32;
                    else if (length == 5) this.width = 40;

                    // We define a property to hold the image's div. We'll
                    // actually create this div upon receipt of the onAdd()
                    // method so we'll leave it null for now.
                    this.div_ = null;

                    // Explicitly call setMap on this overlay
                    this.setMap(options.map);
                }

                TextOverlay.prototype.onAdd = function() {

                    // Create the DIV and set some basic attributes.
                    var div = document.createElement("div");
                    div.style.color = this.color;
                    div.style.fontSize = "15px";
                    div.style.position = "absolute";
                    div.style.zIndex = "999";
                    div.style.fontWeight = "bold";

                    // Create an IMG element and attach it to the DIV.
                    div.innerHTML = this.text;

                    // Set the overlay's div_ property to this DIV
                    this.div_ = div;

                    // We add an overlay to a map via one of the map's panes.
                    // We'll add this overlay to the overlayLayer pane.
                    var panes = this.getPanes();
                    panes.overlayLayer.appendChild(div);
                }

                TextOverlay.prototype.draw = function() {
                    // Size and position the overlay. We use a southwest and northeast
                    // position of the overlay to peg it to the correct position and size.
                    // We need to retrieve the projection from this overlay to do this.
                    var overlayProjection = this.getProjection();

                    // Retrieve the southwest and northeast coordinates of this overlay
                    // in latlngs and convert them to pixels coordinates.
                    // We'll use these coordinates to resize the DIV.
                    var sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest());
                    var ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast());
                    // Resize the image's DIV to fit the indicated dimensions.
                    var div = this.div_;
                    var width = this.width;
                    var height = 20;

                    div.style.left = (sw.x - width / 2) + "px";
                    div.style.top = (ne.y - height / 2) + "px";
                    div.style.width = width + "px";
                    div.style.height = height + "px";
                    div.style.width = width + "px";
                    div.style.height = height + "px";

                }

                TextOverlay.prototype.onRemove = function() {
                    this.div_.parentNode.removeChild(this.div_);
                    this.div_ = null;
                }

                // Determine the geohash level we will use to draw tiles
                var currentZoom = this.map.getZoom(),
                    geohashLevelNum = this.mapModel.determineGeohashLevel(currentZoom),
                    geohashLevel = "geohash_" + geohashLevelNum,
                    geohashes = this.searchResults.facetCounts[geohashLevel];

                // Save the current geohash level in the map model
                this.mapModel.set("tileGeohashLevel", geohashLevelNum);

                // Get all the geohashes contained in the map
                var mapBBoxes = _.flatten(_.values(this.searchModel.get("geohashGroups")));

                // Geohashes may be returned that are part of datasets with multiple geographic areas. Some of these may be outside this map.
                // So we will want to filter out geohashes that are not contained in this map.
                if (mapBBoxes.length == 0) {
                    var filteredTileGeohashes = geohashes;
                } else if( geohashes ){
                    var filteredTileGeohashes = [];
                    for (var i = 0; i < geohashes.length - 1; i += 2) {

                        // Get the geohash for this tile
                        var tileGeohash = geohashes[i],
                            isInsideMap = false,
                            index = 0,
                            searchString = tileGeohash;

                        // Find if any of the bounding boxes/geohashes inside our map contain this tile geohash
                        while ((!isInsideMap) && (searchString.length > 0)) {
                            searchString = tileGeohash.substring(0, tileGeohash.length - index);
                            if (_.contains(mapBBoxes, searchString)) isInsideMap = true;
                            index++;
                        }

                        if (isInsideMap) {
                            filteredTileGeohashes.push(tileGeohash);
                            filteredTileGeohashes.push(geohashes[i + 1]);
                        }
                    }
                }

                //If there are no tiles on the page, the map may have failed to render, so exit.
                if( typeof filteredTileGeohashes == "undefined" || !filteredTileGeohashes.length ){
                  return;
                }

                // Make a copy of the array that is geohash counts only
                var countsOnly = [];
                for (var i = 1; i < filteredTileGeohashes.length; i += 2) {
                    countsOnly.push(filteredTileGeohashes[i]);
                }

                // Create a range of lightness to make different colors on the tiles
                var lightnessMin = this.mapModel.get("tileLightnessMin"),
                    lightnessMax = this.mapModel.get("tileLightnessMax"),
                    lightnessRange = lightnessMax - lightnessMin;

                // Get some stats on our tile counts so we can normalize them to create a color scale
                var findMedian = function(nums) {
                    if (nums.length % 2 == 0) {
                        return (nums[(nums.length / 2) - 1] + nums[(nums.length / 2)]) / 2;
                    } else {
                        return nums[(nums.length / 2) - 0.5];
                    }
                }
                var sortedCounts = countsOnly.sort(function(a, b) {
                        return a - b;
                    }),
                    maxCount = sortedCounts[sortedCounts.length - 1],
                    minCount = sortedCounts[0];

                var viewRef = this;

                // Now draw a tile for each geohash facet
                for (var i = 0; i < filteredTileGeohashes.length - 1; i += 2) {

                    // Convert this geohash to lat,long values
                    var tileGeohash = filteredTileGeohashes[i],
                        decodedGeohash = nGeohash.decode(tileGeohash),
                        latLngCenter = new google.maps.LatLng(decodedGeohash.latitude, decodedGeohash.longitude),
                        geohashBox = nGeohash.decode_bbox(tileGeohash),
                        swLatLng = new google.maps.LatLng(geohashBox[0], geohashBox[1]),
                        neLatLng = new google.maps.LatLng(geohashBox[2], geohashBox[3]),
                        bounds = new google.maps.LatLngBounds(swLatLng, neLatLng),
                        tileCount = filteredTileGeohashes[i + 1],
                        drawMarkers = this.mapModel.get("drawMarkers"),
                        marker,
                        count,
                        color;

                    // Normalize the range of tiles counts and convert them to a lightness domain of 20-70% lightness.
                    if (maxCount - minCount == 0) {
                        var lightness = lightnessRange;
                    } else {
                        var lightness = (((tileCount - minCount) / (maxCount - minCount)) * lightnessRange) + lightnessMin;
                    }

                    var color = "hsl(" + this.mapModel.get("tileHue") + "," + lightness + "%,50%)";

                    // Add the count to the tile
                    var countLocation = new google.maps.LatLngBounds(latLngCenter, latLngCenter);

                    // Draw the tile label with the dataset count
                    count = new TextOverlay({
                        bounds: countLocation,
                        map: this.map,
                        text: tileCount,
                        color: this.mapModel.get("tileLabelColor")
                    });

                    // Set up the default tile options
                    var tileOptions = {
                        fillColor: color,
                        strokeColor: color,
                        map: this.map,
                        visible: true,
                        bounds: bounds
                    };

                    // Merge these options with any tile options set in the map model
                    var modelTileOptions = this.mapModel.get("tileOptions");
                    for (var attr in modelTileOptions) {
                        tileOptions[attr] = modelTileOptions[attr];
                    }

                    // Draw this tile
                    var tile = this.drawTile(tileOptions, tileGeohash, count);

                    // Save the geohashes for tiles in the view for later
                    this.tileGeohashes.push(tileGeohash);
                }

                // Create an info window for each marker that is on the map, to display when it is clicked on
                if (this.markerGeohashes.length > 0) this.addMarkers();

                // If the map is zoomed all the way in, draw info windows for each tile that will be displayed when they are clicked on
                if (this.mapModel.isMaxZoom(this.map)) this.addTileInfoWindows();
            },

            /**
             * With the options and label object given, add a single tile to the map and set its event listeners
             * @param {object} options
             * @param {string} geohash
             * @param {string} label
             **/
            drawTile: function(options, geohash, label) {
                // Exit if maps are not in use
                if ((this.mode != "map") || (!gmaps)) {
                    return false;
                }

                // Add the tile for these datasets to the map
                var tile = new google.maps.Rectangle(options);

                var viewRef = this;

                // Save our tiles in the view
                var tileObject = {
                    text: label,
                    shape: tile,
                    geohash: geohash,
                    options: options
                };
                this.tiles.push(tileObject);

                // Change styles when the tile is hovered on
                google.maps.event.addListener(tile, "mouseover", function(event) {
                    viewRef.highlightTile(tileObject);
                });

                // Change the styles back after the tile is hovered on
                google.maps.event.addListener(tile, "mouseout", function(event) {
                    viewRef.unhighlightTile(tileObject);
                });

                // If we are at the max zoom, we will display an info window. If not, we will zoom in.
                if (!this.mapModel.isMaxZoom(viewRef.map)) {

                    /** Set up some helper functions for zooming in on the map **/
                    var myFitBounds = function(myMap, bounds) {
                        myMap.fitBounds(bounds); // calling fitBounds() here to center the map for the bounds

                        var overlayHelper = new google.maps.OverlayView();
                        overlayHelper.draw = function() {
                            if (!this.ready) {
                                var extraZoom = getExtraZoom(this.getProjection(), bounds, myMap.getBounds());
                                if (extraZoom > 0) {
                                    myMap.setZoom(myMap.getZoom() + extraZoom);
                                }
                                this.ready = true;
                                google.maps.event.trigger(this, "ready");
                            }
                        };
                        overlayHelper.setMap(myMap);
                    }
                    var getExtraZoom = function(projection, expectedBounds, actualBounds) {

                        // in: LatLngBounds bounds -> out: height and width as a Point
                        var getSizeInPixels = function(bounds) {
                            var sw = projection.fromLatLngToContainerPixel(bounds.getSouthWest());
                            var ne = projection.fromLatLngToContainerPixel(bounds.getNorthEast());
                            return new google.maps.Point(Math.abs(sw.y - ne.y), Math.abs(sw.x - ne.x));
                        }

                        var expectedSize = getSizeInPixels(expectedBounds),
                            actualSize = getSizeInPixels(actualBounds);

                        if (Math.floor(expectedSize.x) == 0 || Math.floor(expectedSize.y) == 0) {
                            return 0;
                        }

                        var qx = actualSize.x / expectedSize.x;
                        var qy = actualSize.y / expectedSize.y;
                        var min = Math.min(qx, qy);

                        if (min < 1) {
                            return 0;
                        }

                        return Math.floor(Math.log(min) / Math.LN2 /* = log2(min) */ );
                    }

                    // Zoom in when the tile is clicked on
                    gmaps.event.addListener(tile, "click", function(clickEvent) {
                        // Change the center
                        viewRef.map.panTo(clickEvent.latLng);

                        // Get this tile's bounds
                        var tileBounds = tile.getBounds();
                        // Get the current map bounds
                        var mapBounds = viewRef.map.getBounds();

                        // Change the zoom
                        //viewRef.map.fitBounds(tileBounds);
                        myFitBounds(viewRef.map, tileBounds);


                        // Track this event
                        MetacatUI.analytics?.trackEvent("map", "clickTile", "geohash : " + tileObject.geohash);

                    });
                }

                return tile;
            },

            highlightTile: function(tile) {
                // Change the tile style on hover
                tile.shape.setOptions(this.mapModel.get("tileOnHover"));

                // Change the label color on hover
                var div = tile.text.div_;
                if(div){
                  div.style.color = this.mapModel.get("tileLabelColorOnHover");
                  tile.text.div_ = div;
                  $(div).css("color", this.mapModel.get("tileLabelColorOnHover"));
                }
            },

            unhighlightTile: function(tile) {
                // Change back the tile to it's original styling
                tile.shape.setOptions(tile.options);

                // Change back the label color
                var div = tile.text.div_;
                div.style.color = this.mapModel.get("tileLabelColor");
                tile.text.div_ = div;
                $(div).css("color", this.mapModel.get("tileLabelColor"));
            },

            /**
             * Get the details on each marker
             * And create an infowindow for that marker
             */
            addMarkers: function() {
                // Exit if maps are not in use
                if ((this.mode != "map") || (!gmaps)) {
                    return false;
                }

                // Clone the Search model
                var searchModelClone = this.searchModel.clone(),
                    geohashLevel = this.mapModel.get("tileGeohashLevel"),
                    viewRef = this,
                    markers = this.markers;

                // Change the geohash filter to match our tiles
                searchModelClone.set("geohashLevel", geohashLevel);
                searchModelClone.set("geohashes", this.markerGeohashes);

                // Now run a query to get a list of documents that are represented by our markers
                var query = "q=" + searchModelClone.getQuery() +
                    "&fl=id,title,geohash_9,abstract,geohash_" + geohashLevel +
                    "&rows=1000" +
                    "&wt=json";

                var requestSettings = {
                    url: MetacatUI.appModel.get("queryServiceUrl") + query,
                    success: function(data, textStatus, xhr) {
                        var docs = data.response.docs;
                        var uniqueGeohashes = viewRef.markerGeohashes;

                        // Create a marker and infoWindow for each document
                        _.each(docs, function(doc, key, list) {

                            var marker,
                                drawMarkersAt = [];

                            // Find the tile place that this document belongs to
                            // For each geohash value at the current geohash level for this document,
                            _.each(doc.geohash_9, function(geohash, key, list) {
                                // Loop through each unique tile location to find its match
                                for (var i = 0; i <= uniqueGeohashes.length; i++) {
                                    if (uniqueGeohashes[i] == geohash.substr(0, geohashLevel)) {
                                        drawMarkersAt.push(geohash);
                                        uniqueGeohashes = _.without(uniqueGeohashes, geohash);
                                    }
                                }
                            });

                            _.each(drawMarkersAt, function(markerGeohash, key, list) {

                                var decodedGeohash = nGeohash.decode(markerGeohash),
                                    latLng = new google.maps.LatLng(decodedGeohash.latitude, decodedGeohash.longitude);

                                // Set up the options for each marker
                                var markerOptions = {
                                    position: latLng,
                                    icon: this.mapModel.get("markerImage"),
                                    zIndex: 99999,
                                    map: viewRef.map
                                };

                                // Create the marker and add to the map
                                var marker = new google.maps.Marker(markerOptions);
                            });
                        });
                    }
                }
                $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));

            },

            /**
             * Get the details on each tile - a list of ids and titles for each dataset contained in that tile
             * And create an infowindow for that tile
             */
            addTileInfoWindows: function() {
                // Exit if maps are not in use
                if ((this.mode != "map") || (!gmaps)) {
                    return false;
                }

                // Clone the Search model
                var searchModelClone = this.searchModel.clone(),
                    geohashLevel = this.mapModel.get("tileGeohashLevel"),
                    geohashName = "geohash_" + geohashLevel,
                    viewRef = this,
                    infoWindows = [];

                // Change the geohash filter to match our tiles
                searchModelClone.set("geohashLevel", geohashLevel);
                searchModelClone.set("geohashes", this.tileGeohashes);

                // Now run a query to get a list of documents that are represented by our tiles
                var query = "q=" + searchModelClone.getQuery() +
                    "&fl=id,title,geohash_9," + geohashName +
                    "&rows=1000" +
                    "&wt=json";

                var requestSettings = {
                    url: MetacatUI.appModel.get("queryServiceUrl") + query,
                    success: function(data, textStatus, xhr) {
                        // Make an infoWindow for each doc
                        var docs = data.response.docs;

                        // For each tile, loop through the docs to find which ones to include in its infoWindow
                        _.each(viewRef.tiles, function(tile, key, list) {

                            var infoWindowContent = "";

                            _.each(docs, function(doc, key, list) {

                              var docGeohashes = doc[geohashName];

                              if(docGeohashes){
                                // Is this document in this tile?
                                for (var i = 0; i < docGeohashes.length; i++) {
                                    if (docGeohashes[i] == tile.geohash) {
                                        // Add this doc to the infoWindow content
                                        infoWindowContent += "<a href='" + MetacatUI.root + "/view/" + encodeURIComponent(doc.id) + "'>" + doc.title + "</a> (" + doc.id + ") <br/>"
                                        break;
                                    }
                                }
                              }
                            });

                            // The center of the tile
                            var decodedGeohash = nGeohash.decode(tile.geohash),
                                tileCenter = new google.maps.LatLng(decodedGeohash.latitude, decodedGeohash.longitude);

                            // The infowindow
                            var infoWindow = new gmaps.InfoWindow({
                                content: "<div class='gmaps-infowindow'>" +
                                    "<h4> Datasets located here </h4>" +
                                    "<p>" + infoWindowContent + "</p>" +
                                    "</div>",
                                isOpen: false,
                                disableAutoPan: false,
                                maxWidth: 250,
                                position: tileCenter
                            });

                            viewRef.tileInfoWindows.push(infoWindow);

                            // Zoom in when the tile is clicked on
                            gmaps.event.addListener(tile.shape, "click", function(clickEvent) {

                                //--- We are at max zoom, display an infowindow ----//
                                if (this.mapModel.isMaxZoom(viewRef.map)) {

                                    // Find the infowindow that belongs to this tile in the view
                                    infoWindow.open(viewRef.map);
                                    infoWindow.isOpen = true;

                                    // Close all other infowindows
                                    viewRef.closeInfoWindows(infoWindow);
                                }

                                //------ We are not at max zoom, so zoom into this tile ----//
                                else {
                                    // Change the center
                                    viewRef.map.panTo(clickEvent.latLng);

                                    // Get this tile's bounds
                                    var bounds = tile.shape.getBounds();

                                    // Change the zoom
                                    viewRef.map.fitBounds(bounds);
                                }
                            });

                            // Close the infowindow upon any click on the map
                            gmaps.event.addListener(viewRef.map, "click", function() {
                                infoWindow.close();
                                infoWindow.isOpen = false;
                            });

                            infoWindows[tile.geohash] = infoWindow;
                        });

                        viewRef.infoWindows = infoWindows;
                    }
                }
                $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings()));
            },

            /**
             * Iterate over each infowindow that we have stored in the view and close it.
             * Pass an infoWindow object to this function to keep that infoWindow open/skip it
             * @param {infoWindow} - An infoWindow to keep open
             */
            closeInfoWindows: function(except) {
                var infoWindowLists = [this.markerInfoWindows, this.tileInfoWindows];

                _.each(infoWindowLists, function(infoWindows, key, list) {
                    // Iterate over all the marker infowindows and close all of them except for this one
                    for (var i = 0; i < infoWindows.length; i++) {
                        if ((infoWindows[i].isOpen) && (infoWindows[i] != except)) {
                            // Close this info window and stop looking, since only one of each kind should be open anyway
                            infoWindows[i].close();
                            infoWindows[i].isOpen = false;
                            i = infoWindows.length;
                        }
                    }
                });
            },

            /**
             * Remove all the tiles and text from the map
             **/
            removeTiles: function() {
                // Exit if maps are not in use
                if ((this.mode != "map") || (!gmaps)) {
                    return false;
                }

                // Remove the tile from the map
                _.each(this.tiles, function(tile, key, list) {
                    if (tile.shape) tile.shape.setMap(null);
                    if (tile.text) tile.text.setMap(null);
                });

                // Reset the tile storage in the view
                this.tiles = [];
                this.tileGeohashes = [];
                this.tileInfoWindows = [];
            },

            /**
             * Iterate over all the markers in the view and remove them from the map and view
             */
            removeMarkers: function() {
                // Exit if maps are not in use
                if ((this.mode != "map") || (!gmaps)) {
                    return false;
                }

                // Remove the marker from the map
                _.each(this.markers, function(marker, key, list) {
                    marker.marker.setMap(null);
                });

                // Reset the marker storage in the view
                this.markers = [];
                this.markerGeohashes = [];
                this.markerInfoWindows = [];
            },


            /*
             * ==================================================================================================
             *                                             ADDING RESULTS
             * ==================================================================================================
             */

            /** Add all items in the **SearchResults** collection
             * This loads the first 25, then waits for the map to be
             * fully loaded and then loads the remaining items.
             * Without this delay, the app waits until all records are processed
             */
            addAll: function() {
                // After the map is done loading, then load the rest of the results into the list
                if (this.ready) this.renderAll();
                else {
                    var viewRef = this;
                    var intervalID = setInterval(function() {
                        if (viewRef.ready) {
                            clearInterval(intervalID);
                            viewRef.renderAll();
                        }
                    }, 500);
                }

                // After all the results are loaded, query for our facet counts in the background
                //this.getAutocompletes();
            },

            renderAll: function() {
                // do this first to indicate coming results
                this.updateStats();

                // Remove all the existing tiles on the map
                this.removeTiles();
                this.removeMarkers();

                // Remove the loading class and styling
                this.$results.removeClass("loading");

                // If there are no results, display so
                var numFound = this.searchResults.length;
                if (numFound == 0) {

                    // Add a No Results Found message
                    this.$results.html("<p id='no-results-found'>No results found.</p>");

                    // Remove the loading styles from the map
                    if (gmaps && this.mapModel) {
                        $("#map-container").removeClass("loading");
                    }

                    if (MetacatUI.theme == "arctic") {
                        // When we get new results, check if the user is searching for their own datasets and display a message
                        if ((MetacatUI.appView.dataCatalogView && MetacatUI.appView.dataCatalogView.searchModel.getQuery() == MetacatUI.appUserModel.get("searchModel").getQuery()) && !MetacatUI.appSearchResults.length) {
                            $("#no-results-found").after("<h3>Where are my data sets?</h3><p>If you are a previous ACADIS Gateway user, " +
                                "you will need to take additional steps to access your data sets in the new NSF Arctic Data Center." +
                                "<a href='mailto:support@arcticdata.io'>Send us a message at support@arcticdata.io</a> with your old ACADIS " +
                                "Gateway username and your ORCID identifier (" + MetacatUI.appUserModel.get("username") + "), we will help.</p>");
                        }
                    }
                    return;
                }

                // Clear the results list before we start adding new rows
                this.$results.html("");

                //--First map all the results--
                if (gmaps && this.mapModel) {
                    // Draw all the tiles on the map to represent the datasets
                    this.drawTiles();

                    // Remove the loading styles from the map
                    $("#map-container").removeClass("loading");
                }

                var pid_list = new Array();

                //--- Add all the results to the list ---
                for (i = 0; i < this.searchResults.length; i++) {
                    pid_list.push(this.searchResults.models[i].get("id"));
                };

                if (MetacatUI.appModel.get("displayDatasetMetrics")) {
                    var metricsModel = new MetricsModel({
                        pid_list: pid_list,
                        type: "catalog"
                    });
                    metricsModel.fetch();
                    this.metricsModel = metricsModel;
                }

                //--- Add all the results to the list ---
                for (i = 0; i < this.searchResults.length; i++) {
                    var element = this.searchResults.models[i];
                    if (typeof element !== "undefined") this.addOne(element, this.metricsModel);
                };

                // Initialize any tooltips within the result item
                $(".tooltip-this").tooltip();
                $(".popover-this").popover();

                // Set the autoheight
                this.setAutoHeight();
            },

            /**
             * Add a single SolrResult item to the list by creating a view for it and appending its element to the DOM.
             */
            addOne: function(result) {
                // Get the view and package service URL's
                this.$view_service = MetacatUI.appModel.get("viewServiceUrl");
                this.$package_service = MetacatUI.appModel.get("packageServiceUrl");
                result.set({
                    view_service: this.$view_service,
                    package_service: this.$package_service
                });

                var view = new SearchResultView({
                  model: result,
                  metricsModel: this.metricsModel
                });

                // Add this item to the list
                this.$results.append(view.render().el);

                // map it
                if (gmaps && this.mapModel && (typeof result.get("geohash_9") != "undefined") && (result.get("geohash_9") != null)) {
                    var title = result.get("title");

                    for (var i = 0; i < result.get("geohash_9").length; i++) {
                        var centerGeohash = result.get("geohash_9")[i],
                            decodedGeohash = nGeohash.decode(centerGeohash),
                            position = new google.maps.LatLng(decodedGeohash.latitude, decodedGeohash.longitude),
                            marker = new gmaps.Marker({
                                position: position,
                                icon: this.mapModel.get("markerImage"),
                                zIndex: 99999
                            });
                    }
                }
            },

            /**
            * When the SearchResults collection has an error getting the results,
            * show an error message instead of search results
            * @param {SolrResult} model
            * @param {XMLHttpRequest.response} response
            */
            showError: function(model, response){

              var errorMessage = "";
              var statusCode = response.status;

              if(!statusCode){
                  statusCode = parseInt(response.statusText)
              }

              if(statusCode == 500 && this.solrError500Message){
                errorMessage = this.solrError500Message;
              } else {
                try{
                    errorMessage = $(response.responseText).text();
                  }
                  catch(e){
                    try{
                      errorMessage = JSON.parse(response.responseText).error.msg;
                    }
                    catch(e){
                      errorMessage = "";
                    }
                  }
                  finally{
                    if( typeof errorMessage == "string" && errorMessage.length ){
                      errorMessage = "<p>Error details: " + errorMessage + "</p>";
                    }
                  }
              }

              MetacatUI.appView.showAlert(
                "<h4><i class='icon icon-frown'></i>" + this.solrErrorTitle + ".</h4>" + errorMessage,
                "alert-error",
                this.$results
              );

              this.$results.find(".loading").remove();

            },


            /*
             * ==================================================================================================
             *                                             STYLING THE UI
             * ==================================================================================================
             */
            toggleMapMode: function(e) {
                if (typeof e === "object") {
                    e.preventDefault();
                }

                if (gmaps) {
                    $(".mapMode").toggleClass("mapMode");
                }

                if (this.mode == "map") {
                    MetacatUI.appModel.set("searchMode", "list");
                    this.mode = "list";
                    this.$("#map-canvas").detach();
                    this.setAutoHeight();
                    this.getResults();
                } else if (this.mode == "list") {
                    MetacatUI.appModel.set("searchMode", "map");
                    this.mode = "map";
                    this.renderMap();
                    this.setAutoHeight();
                    this.getResults();
                }
            },

            // Communicate that the page is loading
            loading: function() {
                $("#map-container").addClass("loading");
                this.$results.addClass("loading");

                this.$results.html(this.loadingTemplate({
                    msg: "Searching for data..."
                }));
            },

            // Toggles the collapseable filters sidebar and result list in the default theme
            collapse: function(e) {
                var id = $(e.target).attr("data-collapse");

                $("#" + id).toggleClass("collapsed");
            },

            toggleFilterCollapse: function(e) {
                if (typeof e !== "undefined") {
                    var container = $(e.target).parents(".filter-contain.collapsable");
                } else {
                    var container = this.$(".filter-contain.collapsable");
                }

                // If we can't find a container, then don't do anything
                if (container.length < 1) return;

                // Expand
                if ($(container).is(".collapsed")) {
                    // Toggle the visibility of the collapse/expand icons
                    $(container).find(".expand").hide();
                    $(container).find(".collapse").show();

                    // Cache the height of this element so we can reset it on collapse
                    $(container).attr("data-height", $(container).css("height"));

                    // Increase the height of the container to expand it
                    $(container).css("max-height", "3000px");
                }
                // Collapse
                else {
                    // Toggle the visibility of the collapse/expand icons
                    $(container).find(".collapse").hide();
                    $(container).find(".expand").show();

                    // Decrease the height of the container to collapse it
                    if ($(container).attr("data-height")) {
                        $(container).css("max-height", $(container).attr("data-height"));
                    } else {
                        $(container).css("max-height", "1.5em");
                    }
                }

                $(container).toggleClass("collapsed");
            },

            /*
             * Either hides or shows the "clear all filters" button
             */
            toggleClearButton: function() {
                if (this.searchModel.filterCount() > 0) {
                    this.showClearButton();
                } else {
                    this.hideClearButton();
                }
            },

            // Move the popover element up the page a bit if it runs off the bottom of the page
            preventPopoverRunoff: function(e) {

                // In map view only (because all elements are fixed and you can't scroll)
                if (this.mode == "map") {
                    var viewportHeight = $("#map-container").outerHeight();
                } else {
                    return false;
                }

                if ($(".popover").length > 0) {
                    var offset = $(".popover").offset();
                    var popoverHeight = $(".popover").outerHeight();
                    var topPosition = offset.top;

                    // If pixels are cut off the top of the page, readjust its vertical position
                    if (topPosition < 0) {
                        $(".popover").offset({
                            top: 10
                        });
                    } else {
                        // Else, let's check if it is cut off at the bottom
                        var totalHeight = topPosition + popoverHeight;

                        var pixelsHidden = totalHeight - viewportHeight;

                        var newTopPosition = topPosition - pixelsHidden - 40;

                        // If pixels are cut off the bottom of the page, readjust its vertical position
                        if (pixelsHidden > 0) {
                            $(".popover").offset({
                                top: newTopPosition
                            });
                        }
                    }
                }

            },

            onClose: function() {
                this.stopListening();

                $(".DataCatalog").removeClass("DataCatalog");
                $(".mapMode").removeClass("mapMode");

                if (gmaps) {
                    // unset map mode
                    $("body").removeClass("mapMode");
                    $("#map-canvas").remove();
                }

                // remove everything so we don't get a flicker
                this.$el.html("");
            }
        });
        return DataCatalogView;
    });