Source: src/js/models/connectors/Map-Search-Filters.js

define([
  "backbone",
  "models/maps/Map",
  "collections/SolrResults",
  "collections/Filters",
  "models/connectors/Map-Search",
  "models/connectors/Filters-Search",
  "models/connectors/Filters-Map",
  "models/filters/FilterGroup",
], function (
  Backbone,
  Map,
  SearchResults,
  Filters,
  MapSearchConnector,
  FiltersSearchConnector,
  FiltersMapConnector,
  FilterGroup,
) {
  "use strict";

  /**
   * @class  MapSearchFiltersConnector
   * @classdesc A model that handles connecting the Map model, the SolrResults
   * model, and the Filters model, e.g. for a CatalogSearchView.
   * @name  MapSearchFiltersConnector
   * @extends Backbone.Model
   * @constructor
   * @classcategory Models/Connectors
   * @since 2.25.0
   */
  return Backbone.Model.extend(
    /** @lends  MapSearchFiltersConnector.prototype */ {
      /**
       * The default values for this model.
       * @type {object}
       * @property {Map} map - The Map model to use for this connector.
       * @property {SolrResults} searchResults - The SolrResults model to use
       * @property {Filters} filters - The Filters model to use for this
       */
      defaults: function () {
        return {
          map: null,
          searchResults: null,
          filterGroups: [],
          filters: null,
        };
      },

      /**
       * Initialize the model.
       * @param {Object} attrs - The attributes for this model.
       * @param {Object} options - The options for this model.
       * @param {Map | Object} [options.map] - The Map model to use for this
       * connector or a JSON object with options to create a new Map model. If
       * not provided, the default from the appModel will be used. See
       * {@link AppModel#catalogSearchMapOptions}.
       * @param {SolrResults | Object} [options.searchResults] - The SolrResults
       * model to use for this connector or a JSON object with options to create
       * a new SolrResults model. If not provided, a new SolrResults model will
       * be created.
       * @param {FilterGroup[] | FilterGroup} [options.filterGroups] - An array
       * of FilterGroup models or JSON objects with options to create new
       * FilterGroup models. If a single FilterGroup is passed, it will be
       * wrapped in an array. If not provided, the default from the appModel
       * will be used. See {@link AppModel#defaultFilterGroups}.
       * @param {boolean} [options.addGeohashLayer=true] - If set to true, a Geohash
       * layer will be added to the Map model if one is not already present. If
       * set to false, no Geohash layer will be added. A geohash layer is
       * required for the Search-Map connector to work.
       * @param {boolean} [options.addSpatialFilter=true] - If set to true, a spatial
       * filter will be added to the Filters model if one is not already
       * present. If set to false, no spatial filter will be added. A spatial
       * filter is required for the Filters-Map connector to work.
       * @param {boolean} [options.catalogSearch=false] - If set to true, a
       * catalog search phrase in the Filters will be appended to the search
       * query that limits the results to un-obsoleted metadata. See
       * {@link Filters#createCatalogSearchQuery}.If set to true, a catalog
       * search phrase will be appended to the search query that limits the
       * results to un-obsoleted metadata.
       */
      initialize: function (attrs, options = {}) {
        if (!options) options = {};
        const app = MetacatUI.appModel;
        const map = options.map || app.get("catalogSearchMapOptions") || {};
        const searchResults = options.searchResults || null;
        const filterGroups =
          options.filterGroups || app.get("defaultFilterGroups");
        const catalogSearch = options.catalogSearch !== true;
        const addGeohashLayer = options.addGeohashLayer !== false;
        const addSpatialFilter = options.addGeohashLayer !== false;
        this.setMap(map);
        this.setSearchResults(searchResults);
        this.setFilters(filterGroups, catalogSearch);
        this.setConnectors(addGeohashLayer, addSpatialFilter);
      },

      /**
       * Set the Map model for this connector.
       * @param {Map | Object } map - The Map model to use for this connector or
       * a JSON object with options to create a new Map model.
       */
      setMap: function (map) {
        const mapModel = map instanceof Map ? map : new Map(map || null);
        this.set("map", mapModel);
      },

      /**
       * Set the SearchResults model for this connector.
       * @param {SolrResults | Object } searchResults - The SolrResults model to
       * use for this connector or a JSON object with options to create a new
       * SolrResults model.
       */
      setSearchResults: function (searchResults) {
        const resultsModel =
          searchResults instanceof SearchResults
            ? searchResults
            : new SearchResults(searchResults || {});
        this.set("searchResults", resultsModel);
      },

      /**
       * Set the Filters model for this connector.
       * @param {Array} filters - An array of FilterGroup models or JSON objects
       * with options to create new FilterGroup models. If a single FilterGroup
       * is passed, it will be wrapped in an array.
       * @param {boolean} [catalogSearch=true] - If true, the Filters model will
       * be created with the catalogSearch option set to true.
       * @see Filters
       * @see FilterGroup
       */
      setFilters: function (filtersArray, catalogSearch = true) {
        const filterGroups = [];
        const filters = new Filters(null, { catalogSearch: catalogSearch });

        filtersArray = Array.isArray(filtersArray)
          ? filtersArray
          : [filtersArray];

        filtersArray.forEach((filterGroup) => {
          const filterGroupModel =
            filterGroup instanceof FilterGroup
              ? filterGroup
              : new FilterGroup(filterGroup || {});
          filterGroups.push(filterGroupModel);
          filters.add(filterGroupModel.get("filters").models);
        });

        this.set("filterGroups", filterGroups);
        this.set("filters", filters);
      },

      /**
       * Set all the connectors required to connect the Map, SearchResults, and
       * Filters. This does not connect them (see connect()).
       * @param {boolean} [addGeohashLayer=true] - If set to true, a Geohash
       * layer will be added to the Map model if one is not already present. If
       * set to false, no Geohash layer will be added. A geohash layer is
       * required for the Search-Map connector to work.
       * @param {boolean} [addSpatialFilter=true] - If set to true, a spatial
       * filter will be added to the Filters model if one is not already
       * present. If set to false, no spatial filter will be added. A spatial
       * filter is required for the Filters-Map connector to work.
       */
      setConnectors: function (
        addGeohashLayer = true,
        addSpatialFilter = true,
      ) {
        const map = this.get("map");
        const searchResults = this.get("searchResults");
        const filters = this.get("filters");

        this.set(
          "mapSearchConnector",
          new MapSearchConnector({ map, searchResults }, { addGeohashLayer }),
        );
        this.set(
          "filtersSearchConnector",
          new FiltersSearchConnector({ filters, searchResults }),
        );
        this.set(
          "filtersMapConnector",
          new FiltersMapConnector({ filters, map }, { addSpatialFilter }),
        );
      },

      /**
       * Get all the connectors associated with this connector.
       * @returns {Backbone.Model[]} An array of connector models.
       */
      getConnectors: function () {
        return [
          this.get("mapSearchConnector"),
          this.get("filtersSearchConnector"),
          this.get("filtersMapConnector"),
        ];
      },

      /**
       * Get all the connectors associated with the Map.
       * @returns {Backbone.Model[]} An array of connector models.
       */
      getMapConnectors: function () {
        return [
          this.get("mapSearchConnector"),
          this.get("filtersMapConnector"),
        ];
      },

      /**
       * Set all necessary listeners between the Map, SearchResults, and Filters
       * so that they work together.
       */
      connect: function () {
        this.disconnect();
        this.coordinateMoveEndSearch();
        this.getConnectors().forEach((connector) => connector.connect());
      },

      /**
       * Disconnect all listeners between the Map, SearchResults, and Filters.
       * @param {boolean} [resetSpatialFilter=false] - If true, the spatial
       * filter will be reset to the default value, which will effectively
       * remove any spatial constraints from the search.
       */
      disconnect: function (resetSpatialFilter = false) {
        this.get("filtersMapConnector").disconnect(resetSpatialFilter);
        this.get("filtersSearchConnector").disconnect();
        this.get("mapSearchConnector").disconnect();
        this.resetMoveEndSearch();
      },

      /**
       * Coordinate behaviour between the two map related sub-connectors when
       * the map extent changes. This is necessary to reduce the number of
       * search queries. We keep the moveEnd behaviour within the sub-connectors
       * so that each sub-connector still functions independently from this
       * coordinating connector.
       */
      coordinateMoveEndSearch: function () {
        // Undo any previous coordination, if any
        this.resetMoveEndSearch();

        const map = this.get("map");
        const interactions = map.get("interactions");
        const mapConnectors = this.getMapConnectors();

        // Stop the sub-connectors from doing anything on moveEnd by setting
        // their method they call on moveEnd to null
        mapConnectors.forEach((connector) => {
          connector.set("onMoveEnd", null);
        });

        // Set the single moveEnd listener here, and run the default moveEnd
        // behaviour for each sub-connector. This effectively triggers only one
        // search per moveEnd.
        this.listenTo(interactions, "moveEnd", function () {
          mapConnectors.forEach((connector) => {
            const moveEndFunc = connector.defaults().onMoveEnd;
            if (typeof moveEndFunc === "function") {
              moveEndFunc.call(connector);
            }
          });
        });
      },

      /**
       * Undo the coordination of the two map related sub-connectors when the
       * map extent changes. Reset the moveEnd behaviour of the sub-connectors
       * to their defaults.
       * @see coordinateMoveEndSearch
       */
      resetMoveEndSearch: function () {
        this.stopListening(this.get("map").get("interactions"), "moveEnd");
        const mapConnectors = this.getMapConnectors();
        mapConnectors.forEach((connector) => {
          connector.set("onMoveEnd", connector.defaults().onMoveEnd);
        });
      },

      /**
       * Disconnect the filters from the map. This stops the map from updating
       * any spatial filters in the filters collection with the extent of the
       * map view.
       * @param {boolean} [resetSpatialFilter=false] - If true, the spatial
       * filter will be reset to the default value, which will effectively
       * remove any spatial constraints from the search.
       */
      disconnectFiltersMap: function (resetSpatialFilter = false) {
        const [mapSearch, filtersMap] = this.getMapConnectors();

        if (mapSearch.get("isConnected")) {
          this.resetMoveEndSearch();
          mapSearch.set("onMoveEnd", mapSearch.defaults().onMoveEnd);
        }

        filtersMap.disconnect(resetSpatialFilter);
      },

      /**
       * Connect or re-connect the filters to the map. This will enable the map
       * to start updating any spatial filters in the filters collection with
       * the extent of the map view.
       */
      connectFiltersMap: function () {
        const [mapSearch, filtersMap] = this.getMapConnectors();

        if (mapSearch.get("isConnected")) {
          this.coordinateMoveEndSearch();
        }
        filtersMap.connect();
      },

      /**
       * Check if all connectors are connected.
       * @returns {boolean} True if all connectors are connected, false if any
       * are disconnected.
       */
      isConnected: function () {
        const connectors = this.getConnectors();
        return connectors.every((connector) => connector.get("isConnected"));
      },

      /**
       * Remove the spatial filter from the Filters model.
       */
      removeSpatialFilter: function () {
        this.get("filtersMapConnector").removeSpatialFilter();
      },
    },
  );
});