Source: src/js/views/DataCatalogViewWithFilters.js

define([
  "jquery",
  "underscore",
  "backbone",
  "gmaps",
  "collections/Filters",
  "collections/SolrResults",
  "models/filters/FilterGroup",
  "models/filters/SpatialFilter",
  "models/Stats",
  "views/DataCatalogView",
  "views/filters/FilterGroupsView",
  "text!templates/dataCatalog.html",
  "nGeohash",
], function (
  $,
  _,
  Backbone,
  gmaps,
  Filters,
  SearchResults,
  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. This view is deprecated and will eventually be
   * removed in a future version (likely 3.0.0)
   * @classcategory Views
   * @extends DataCatalogView
   * @constructor
   * @deprecated
   */
  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),

      /**
       * A reference to the PortalEditorView
       * @type {PortalEditorView}
       */
      editorView: undefined,

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

        if (!this.searchResults) {
          this.searchResults = new SearchResults();
        }

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

        this.addAnnotationFilter();

        return this;
      },

      /**
       * Creates UI Filter Groups and renders them in this view. UI Filter
       * Groups are custom, interactive search filter elements, grouped together
       * in one panel, section, tab, etc.
       */
      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,
          editorView: this.editorView,
        });

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

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

        // If the query hasn't changed since the last query that was sent, don't
        // do anything. This function may have been triggered by a change event
        // on a filter that doesn't affect the query at all
        if (query == this.searchResults.getLastQuery()) {
          return;
        }

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

        // Specify which fields to retrieve
        var fields = [
          "id",
          "seriesId",
          "title",
          "origin",
          "pubDate",
          "dateUploaded",
          "abstract",
          "resourceMap",
          "beginDate",
          "endDate",
          "read_count_i",
          "geohash_9",
          "datasource",
          "isPublic",
          "project",
          "documents",
          "label",
          "logo",
          "formatId",
        ];
        // Add spatial fields if the map is present
        if (gmaps) {
          fields.push(
            "northBoundCoord",
            "southBoundCoord",
            "eastBoundCoord",
            "westBoundCoord",
          );
        }
        // Set the field list on the SolrResults collection as a comma-separated
        // string
        this.searchResults.setfields(fields.join(","));

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

        // Set the query on the SolrResults collection
        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, which triggers a search
        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");

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

      /**
       * 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) {
        // 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 viewRef;

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

        // 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;
        viewRef = this;

        // Listen to idle events on the map (at rest), and render content as
        // needed
        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) {
            // 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
            viewRef.mapModel.get("mapOptions").center = mapRef.getCenter();
            viewRef.mapModel.get("mapOptions").zoom = mapRef.getZoom();

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

            precision = viewRef.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 (viewRef.searchModel.get("useGeohash")) {
              viewRef.searchModel.get("filters").add(spatialFilter);

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

                // When the custom spatial filter is removed in the UI, toggle
                // the map filter
                viewRef.listenTo(
                  viewRef.filterGroupsView,
                  "customAppliedFilterRemoved",
                  function (removedFilter) {
                    if (removedFilter.type == "SpatialFilter") {
                      // Uncheck the map filter on the map itself
                      viewRef.$(".toggle-map-filter").prop("checked", false);
                      viewRef.toggleMapFilter();
                    }
                  },
                );
              }
            }
          } 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;
          }
        });

        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;
        });
      },
    },
  );
  return DataCatalogViewWithFilters;
});