Source: js/views/DataCatalogViewWithFilters.js

define(["jquery",
        "underscore",
        "backbone",
        "gmaps",
        "collections/Filters",
        "models/filters/FilterGroup",
        "models/filters/SpatialFilter",
        "models/Stats",
        "views/DataCatalogView",
        "views/filters/FilterGroupsView",
        "text!templates/dataCatalog.html",
        "nGeohash"
    ],
    function($, _, Backbone, gmaps, Filters, FilterGroup, SpatialFilter, Stats,
      DataCatalogView, FilterGroupsView,
      template, nGeohash) {

        /**
         * @class DataCatalogViewWithFilters
         * @classdesc A DataCatalogView that uses the Search collection
         * and the Filter models for managing queries rather than the
         * Search model and the filter literal objects used in the
         * parent DataCatalogView.  This accommodates custom portal filters.
         * @extends DataCatalogView
         * @constructor
         */
        var DataCatalogViewWithFilters = DataCatalogView.extend(
          /** @lends DataCatalogViewWithFilters.prototype */{

            el: null,

            /**
            * The HTML tag name for this view element
            * @type {string}
            */
            tagName: "div",

            /**
            * The HTML class names for this view element
            * @type {string}
            */
            className: "data-catalog",

            /**
            * The primary HTML template for this view
            * @type {Underscore.template}
            */
            template: _.template(template),

            /**
            * The sort order for the Solr query
            * @type {string}
            */
            sortOrder: "dateUploaded+desc",

            /**
            * The jQuery selector for the FilterGroupsView container
            * @type {string}
            */
            filterGroupsContainer: ".filter-groups-container",

            /**
            * The Search model to use for creating and storing Filters and contructing query strings.
            * This property is a Search model instead of a Filters collection in
            * order to be quickly compatible with the superclass/superview, DataCatalogView,
            * which was created with the (eventually to be deprecated) SearchModel.
            * A Filters collection is set on the Search model and does most of the work
            * for creating queries.
            * @type (Search)
            */
            searchModel: undefined,

            /**
             * Override DataCatalogView.render() to render this view with filters
             * from the Filters collection
             */
            render: function() {
                var loadingHTML;
                var templateVars;
                var compiledEl;
                var tooltips;
                var groupedTooltips;
                var forFilterLabel = true;
                var forOtherElements = false;
                // TODO: Do we really need to cache the filters collection?
                // Reconcile this from DataCatalogView.render()
                // See https://github.com/NCEAS/metacatui/blob/19d608df9cc17ac2abee76d35feca415137c09d7/src/js/views/DataCatalogView.js#L122-L145

                //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(!this.statsModel){
                  this.statsModel = new Stats();
                }

                // Use map mode on tablets and browsers only
                if ($(window).outerWidth() <= 600) {
                    this.mode = "list";
                    MetacatUI.appModel.set("searchMode", "list");
                    gmaps = null;
                }

                // If this is a subview, don't set the headerType
                if (!this.isSubView) {
                    MetacatUI.appModel.set("headerType", "default");
                    $("body").addClass("DataCatalog");
                } else {
                    this.$el.addClass("DataCatalog");
                }
                //Populate the search template with some model attributes
                loadingHTML = this.loadingTemplate({
                    msg: "Loading entries ..."
                });

                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"
                }
                compiledEl =
                    this.template(_.extend(this.searchModel.toJSON(), templateVars));
                this.$el.html(compiledEl);

                //Create and render the FilterGroupsView
                this.createFilterGroups();

                // 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
                tooltips = $(".tooltip-this");

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

                $(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"
                });

                // 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);

                // Listen to changes in the Search model Filters to trigger a search
                this.stopListening(this.searchModel.get("filters"), "add remove update reset change");
                this.listenTo(this.searchModel.get("filters"), "add remove update reset change", this.triggerSearch);

                // 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);
                }

                if (MetacatUI.appModel.get("bioportalAPIKey")) {
                    this.setUpTree();
                }

                return this;
            },

            /**
            * Creates filter groups and renders them in this view
            */
            createFilterGroups: function(){

              //If it was already created, then exit
              if( this.filterGroupsView ){
                return;
              }

              //Start an array for the FilterGroups and the individual Filter models
              var filterGroups = [],
                  allFilters   = [];

              //Iterate over each default FilterGroup in the app config and create a FilterGroup model
              _.each( MetacatUI.appModel.get("defaultFilterGroups"), function(filterGroupJSON){

                //Create the FilterGroup model
                var filterGroup = new FilterGroup(filterGroupJSON);

                //Add to the array
                filterGroups.push(filterGroup);

                //Add the Filters to the array
                allFilters = _.union(allFilters, filterGroup.get("filters").models);

              }, this);

              //Add the filters to the Search model
              this.searchModel.get("filters").add(allFilters);

              //Create a FilterGroupsView
              var filterGroupsView = new FilterGroupsView({
                filterGroups: filterGroups,
                filters: this.searchModel.get("filters"),
                vertical: true,
                parentView: this
              });

              //Add the FilterGroupsView element to this view
              this.$(this.filterGroupsContainer).html(filterGroupsView.el);

              //Render the FilterGroupsView
              filterGroupsView.render();

              //Save a reference to the FilterGroupsView
              this.filterGroupsView = filterGroupsView;

            },

            /*
             * Get Results from the Solr index by combining the Filter query string fragments
             * in each Filter instance in the Search collection and querying Solr.
             *
             * Overrides DataCatalogView.getResults().
             */
            getResults: function() {
                var sortOrder = this.searchModel.get("sortOrder");
                var query; // The full query string
                var geohashLevel; // The geohash level to search
                var page; // The page of search results to render
                var position; // The geohash level position in the facet array

                if ( sortOrder ) {
                    this.searchResults.setSort(sortOrder);
                }

                //Specify which fields to retrieve
                var fields = [];
                    fields.push("id");
                    fields.push("seriesId");
                    fields.push("title");
                    fields.push("origin");
                    fields.push("pubDate");
                    fields.push("dateUploaded");
                    fields.push("abstract");
                    fields.push("resourceMap");
                    fields.push("beginDate");
                    fields.push("endDate");
                    fields.push("read_count_i");
                    fields.push("geohash_9");
                    fields.push("datasource");
                    fields.push("isPublic");
                    fields.push("documents");
                // Add spatial fields if the map is present
                if ( gmaps ) {
                    fields.push("northBoundCoord");
                    fields.push("southBoundCoord");
                    fields.push("eastBoundCoord");
                    fields.push("westBoundCoord");
                }
                this.searchResults.setfields(fields.join(","));

                // Get the Solr query string from the Search filter collection
                query = this.searchModel.get("filters").getQuery();

                // Specify which geohash level is used to return tile counts
                if ( gmaps && this.map ) {
                    geohashLevel = "geohash_" +
                        this.mapModel.determineGeohashLevel(this.map.zoom);
                    // Does it already exist as a facet field?
                    position = this.searchResults.facet.indexOf(geohashLevel);
                    if ( position == -1) {
                        this.searchResults.facet.push(geohashLevel);
                    }
                }

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

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

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

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

            /**
             * Toggle the map filter to include or exclude it from the Solr query
             */
            toggleMapFilter: function(event) {
                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 (event && event.target.tagName != "INPUT") {
                    isOn = !isOn;
                    toggleInput.prop("checked", isOn);
                }

                var spatialFilter = _.findWhere(this.searchModel.get("filters").models, {type: "SpatialFilter"});

                if (isOn) {
                    this.searchModel.set("useGeohash", true);

                    if( this.filterGroupsView && spatialFilter ){

                      this.filterGroupsView.addCustomAppliedFilter(spatialFilter);

                    }

                } else {
                    this.searchModel.set("useGeohash", false);
                    // Remove the spatial filter from the collection
                    this.searchModel.get("filters").remove(spatialFilter);

                    if( this.filterGroupsView && spatialFilter ){
                      this.filterGroupsView.removeCustomAppliedFilter(spatialFilter);
                    }
                }

                // Tell the map to trigger a new search and redraw tiles
                this.allowSearch = true;
                google.maps.event.trigger(this.mapModel.get("map"), "idle");

                // Send this event to Google Analytics
                if (MetacatUI.appModel.get("googleAnalyticsKey") && (typeof ga !== "undefined")) {
                    var action = isOn ? "on" : "off";
                    ga("send", "event", "map", action);
                }
            },

            /**
             * Overload this function with an empty function since the Clear button
             * has been moved to the FilterGroupsView
             */
            toggleClearButton: function(){},

            /**
             * Overload this function with an empty function since the Clear button
             * has been moved to the FilterGroupsView
             */
            hideClearButton: function(){},

            /**
             * Overload this function with an empty function since the Clear button
             * has been moved to the FilterGroupsView
             */
            showClearButton: function(){},


            /**
             * Toggle between map and list mode
             *
             * @param(Event)  the event passed by clicking the toggle-map class button
             */
            toggleMapMode: function(event) {
                console.log(event);

                // Block the event from bubbling
                if (typeof event === "object") {
                    event.preventDefault();
                }

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

                // Toggle the mode
                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();
                }
            },

            /**
             * Reset the map to the defaults
             */
            resetMap: function() {

                // The spatial models registered in the filters collection
                var spatialModels;

                if (!gmaps) {
                    return;
                }

                // Remove the SpatialFilter from the collection silently
                // so we don't immediately trigger a new search
                spatialModels =
                    _.where(this.searchModel.get("filters").models, {type: "SpatialFilter"});
                this.searchModel.get("filters").remove(spatialModels, {"silent": true});

                // Reset the map options to defaults
                this.mapModel.set("mapOptions", this.mapModel.defaults().mapOptions);
                this.allowSearch = false;
            },

            /**
             * Render the map based on the mapModel properties and search results
             */
            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;
                }

                // The spatial filter instance used to constrain the search by zoom and extent
                var spatialFilter;

                // The map's configuration
                var mapOptions;

                // The map extent
                var boundingBox;

                // The map bounding coordinates
                var north;
                var west;
                var south;
                var east;

                // The map zoom level
                var zoom;

                // The map geohash precision based on the zoom level
                var precision;

                // The geohash boxes associated with the map extent and zoom
                var geohashBBoxes;

                // References to the map and catalog view instances for callbacks
                var mapRef;
                var catalogViewRef;

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

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

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

                // Get the existing spatial filter if it exists
                if (this.searchModel.get("filters") &&
                    this.searchModel.get("filters")
                        .where({type: "SpatialFilter"}).length > 0) {
                    spatialFilter = this.searchModel.get("filters")
                        .where({type: "SpatialFilter"})[0];
                } else {
                    spatialFilter = new SpatialFilter();
                }

                // Store references
                mapRef = this.map;
                catalogViewRef = this;

                // Listen to idle events on the map (at rest), and render content as needed
                google.maps.event.addListener(mapRef, "idle", function() {
                    catalogViewRef.ready = true;

                    // Remove all markers from the map
                    for (var i = 0; i < catalogViewRef.resultMarkers.length; i++) {
                        catalogViewRef.resultMarkers[i].setMap(null);
                    }
                    catalogViewRef.resultMarkers = new Array();

                    // Trigger a resize so the map background image tiles load completely
                    google.maps.event.trigger(mapRef, "resize");

                    var currentMapCenter = catalogViewRef.mapModel.get("map").getCenter(),
                        savedMapCenter = catalogViewRef.mapModel.get("mapOptions").center,
                        needsRecentered = (currentMapCenter != savedMapCenter);

                    // If we are doing a new search
                    if ( catalogViewRef.allowSearch ) {

                        // If the map is at the minZoom, i.e. zoomed out all the way so the whole world is visible, do not apply the spatial filter
                        if (catalogViewRef.map.getZoom() == mapOptions.minZoom) {
                            if (!catalogViewRef.hasZoomed) {
                                if (needsRecentered && !catalogViewRef.hasDragged) {
                                    catalogViewRef.mapModel.get("map").setCenter(savedMapCenter);
                                }
                                return;
                            }

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

                            catalogViewRef.resetMap();
                        } else {
                            // If the user has not zoomed or dragged to a new area of the map yet
                            // and our map is off-center, recenter it
                            if (!catalogViewRef.hasZoomed && needsRecentered) {
                                catalogViewRef.mapModel.get("map").setCenter(savedMapCenter);
                            }

                            // Show the map filter toggle element
                            catalogViewRef.$(catalogViewRef.mapFilterToggle).show();

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

                            // Set the search model's spatial filter properties
                            // Encode the Google Map bounding box into geohash
                            if ( typeof boundingBox !== "undefined") {
                                north = boundingBox.getNorthEast().lat();
                                west = boundingBox.getSouthWest().lng();
                                south = boundingBox.getSouthWest().lat();
                                east = boundingBox.getNorthEast().lng();
                            }

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

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

                            precision = catalogViewRef.mapModel.getSearchPrecision(zoom);

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

                            // Save our geohash search settings
                            spatialFilter.set({
                                "geohashes": geohashBBoxes,
                                "geohashLevel": precision,
                                "north": north,
                                "west": west,
                                "south": south,
                                "east": east,
                            });

                            // Add the spatial filter to the filters collection if enabled
                            if ( catalogViewRef.searchModel.get("useGeohash") ) {

                                catalogViewRef.searchModel.get("filters").add(spatialFilter);

                                if( catalogViewRef.filterGroupsView && spatialFilter ){
                                  catalogViewRef.filterGroupsView.addCustomAppliedFilter(spatialFilter);

                                  //When the custom spatial filter is removed in the UI, toggle the map filter
                                  catalogViewRef.listenTo( catalogViewRef.filterGroupsView, "customAppliedFilterRemoved", function(removedFilter){

                                      if( removedFilter.type == "SpatialFilter" ){

                                        //Uncheck the map filter on the map itself
                                        catalogViewRef.$(".toggle-map-filter").prop("checked", false);
                                        catalogViewRef.toggleMapFilter();

                                      }

                                  });

                                }

                            } else {
                                // If we have zoomed but have not added or removed filters, we
                                // still trigger a search to update the facet tiles
                                if ( catalogViewRef.hasZoomed ) {
                                    catalogViewRef.triggerSearch();
                                }
                            }
                        }
                        // Reset to the first page
                        if (catalogViewRef.hasZoomed) {
                            MetacatUI.appModel.set("page", 0);
                        }
                        catalogViewRef.allowSearch = false;

                    } else {
                        // Else, if this is the fresh map render on page load
                        if (needsRecentered && !catalogViewRef.hasDragged) {
                            catalogViewRef.mapModel.get("map").setCenter(savedMapCenter);
                        }

                        //Show the map filter toggle element
                        if (catalogViewRef.map.getZoom() > mapOptions.minZoom) {
                            catalogViewRef.$(catalogViewRef.mapFilterToggle).show();
                        }
                    }

                    catalogViewRef.hasZoomed = false;
                });

                // When the user has zoomed the map, trigger a new search, idle event follows
                google.maps.event.addListener(mapRef, "zoom_changed", function() {
                    catalogViewRef.allowSearch = true;
                    catalogViewRef.hasZoomed = true;

                });

                // When the user has dragged the map, don't load cached results.
                // We still may not trigger a new search because the user has to zoom in first,
                // after the map initially loads at full-world view. Idel event follows
                google.maps.event.addListener(mapRef, "dragend", function() {
                    catalogViewRef.hasDragged = true;
                    if (catalogViewRef.map.getZoom() > mapOptions.minZoom) {
                        catalogViewRef.hasZoomed = true;
                        catalogViewRef.allowSearch = true;
                    }
                });
            }
        });
        return DataCatalogViewWithFilters;
    });