Source: src/js/models/Stats.js

define(["jquery", "underscore", "backbone", "promise"], function (
  $,
  _,
  Backbone,
  Promise,
) {
  "use strict";

  /**
   * @class Stats
   * @classdesc This model contains all a collection of statistics/metrics about a collection of DataONE objects
   * @classcategory Models
   * @name Stats
   * @extends Backbone.Model
   * @constructor
   */
  var Stats = Backbone.Model.extend(
    /** @lends Stats.prototype */ {
      /**
       * Default attributes for Stats models
       * @type {Object}
       * @property {string} query - The base query that defines the data collection to get statistis about.
       * @property {string} postQuery - A copy of the `query`, but without any URL encoding
       * @property {boolean} isSystemMetadataQuery - If true, the `query` set on this model is only filtering on system metadata fields which are common between both metadata and data objects.
       * @property {number} metadataCount - The number of metadata objects in this data collection @readonly
       * @property {number} dataCount - The number of data objects in this data collection
       * @property {number} totalCount - The number of metadata and data objects in this data collection. Essentially this is the sum of metadataCount and dataCount
       * @property {number|string[]} metadataFormatIDs - An array of metadata formatIds and the number of metadata objects with that formatId. Uses same structure as Solr facet counts: ["text/csv", 5]
       * @property {number|string[]} dataFormatIDs - An array of data formatIds and the number of data objects with that formatId. Uses same structure as Solr facet counts: ["text/csv", 5]
       * @property {string} firstUpdate - The earliest upload date for any object in this collection, excluding uploads of obsoleted objects
       * @property {number|string[]} dataUpdateDates - An array of date strings and the number of data objects uploaded on that date. Uses same structure as Solr facet counts: ["2015-08-02", 5]
       * @property {number|string[]} metadataUpdateDates An array of date strings and the number of data objects uploaded on that date. Uses same structure as Solr facet counts: ["2015-08-02", 5]
       * @property {string} firstBeginDate - An ISO date string of the earliest year that this data collection describes, from the science metadata
       * @property {string} lastEndDate - An ISO date string of the latest year that this data collection describes, from the science metadata
       * @property {string} firstPossibleDate - The first possible date (as a string) that data could have been collected. This is to weed out badly formatted dates when sending queries.
       * @property {object} temporalCoverage A simple object of date ranges (the object key) and the number of metadata objects uploaded in that date range (the object value). Example: { "1990-2000": 5 }
       * @property {number} queryCoverageFrom - The year to start the temporal coverage range query
       * @property {number} queryCoverageUntil - The year to end the temporal coverage range query
       * @property {number} metadataTotalSize - The total number of bytes of all metadata files
       * @property {number} dataTotalSize - The total number of bytes of all data files
       * @property {number} totalSize - The total number of bytes or metadata and data files
       * @property {boolean} hideMetadataAssessment - If true, metadata assessment scores will not be retrieved
       * @property {Image} mdqScoresImage - The Image objet of an aggregated metadata assessment chart
       * @property {number} maxQueryLength - The maximum query length that will be sent via GET to the query service. Queries that go beyond this length will be sent via POST, if POST is enabled in the AppModel
       * @property {string} mdqImageId - The identifier to use in the request for the metadata assessment chart
       */
      defaults: function () {
        return {
          query: "*:* ",
          postQuery: "",
          isSystemMetadataQuery: false,

          metadataCount: 0,
          dataCount: 0,
          totalCount: 0,

          metadataFormatIDs: [],
          dataFormatIDs: [],

          firstUpload: 0,
          totalUploads: 0,
          metadataUploads: null,
          dataUploads: null,
          metadataUploadDates: null,
          dataUploadDates: null,

          firstUpdate: null,
          dataUpdateDates: null,
          metadataUpdateDates: null,

          firstBeginDate: null,
          lastEndDate: null,
          firstPossibleDate: "1800-01-01T00:00:00Z",
          temporalCoverage: 0,
          queryCoverageFrom: null,
          queryCoverageUntil: null,

          metadataTotalSize: null,
          dataTotalSize: null,
          totalSize: null,

          hideMetadataAssessment: false,
          mdqScoresImage: null,
          mdqImageId: null,

          //HTTP GET requests are typically limited to 2,083 characters. So query lengths
          // should have this maximum before switching over to HTTP POST
          maxQueryLength: 2000,
        };
      },

      /**
       * This function serves as a shorthand way to get all of the statistics stored in the model
       */
      getAll: function () {
        // Only get the MetaDIG scores if MetacatUI is configured to display metadata assesments
        // AND this model has them enabled, too.
        if (
          !MetacatUI.appModel.get("hideSummaryMetadataAssessment") &&
          !this.get("hideMetadataAssessment")
        ) {
          this.getMdqScores();
        }

        //Send the call the get both the metadata and data stats
        this.getMetadataStats();
        this.getDataStats();
      },

      /**
       * Queries for statistics about metadata objects
       */
      getMetadataStats: function () {
        var query = this.get("query"),
          //Filter out the portal and collection documents
          filterQuery =
            "-formatId:*dataone.org/collections* AND -formatId:*dataone.org/portals* AND formatType:METADATA AND -obsoletedBy:*",
          //Use the stats feature to get the sum of the file size
          stats = "true",
          statsField = "size",
          //Get the facet counts for formatIds
          facet = "true",
          facetFormatIdField = "formatId",
          facetFormatIdMin = "1",
          facetFormatIdMissing = "false",
          facetLimit = "-1",
          //Get the upload counts for each month
          facetRange = "dateUploaded",
          facetRangeGap = "+1MONTH",
          facetRangeStart = "1900-01-01T00:00:00.000Z",
          facetRangeEnd = new Date().toISOString(),
          facetMissing = "true",
          //Query for the temporal coverage ranges
          facetQueries = [],
          facetBeginDateField = "beginDate",
          facetEndDateField = "endDate",
          facetDateMin = "1",
          facetDateMissing = "false",
          //Don't return any result docs
          rows = "0",
          //Use JSON for the response format
          wt = "json";

        //How many years back should we look for temporal coverage?
        var lastYear =
            this.get("queryCoverageUntil") || new Date().getUTCFullYear(),
          firstYear = this.get("queryCoverageFrom") || 1950,
          totalYears = lastYear - firstYear,
          today = new Date().getUTCFullYear(),
          yearsFromToday = {
            fromBeginning: today - firstYear,
            fromEnd: today - lastYear,
          };

        //Determine our year range/bin size
        var binSize = 1;

        if (totalYears > 10 && totalYears <= 20) {
          binSize = 2;
        } else if (totalYears > 20 && totalYears <= 50) {
          binSize = 5;
        } else if (totalYears > 50 && totalYears <= 100) {
          binSize = 10;
        } else if (totalYears > 100) {
          binSize = 25;
        }

        //Count all the datasets with coverage before the first year in the year range queries
        var beginDateLimit = new Date(
          Date.UTC(firstYear - 1, 11, 31, 23, 59, 59, 999),
        );
        facetQueries.push(
          "{!key=<" +
            firstYear +
            "}(beginDate:[* TO " +
            beginDateLimit.toISOString() +
            "/YEAR])",
        );

        //Construct our facet.queries for the beginDate and endDates, starting with all years before this current year
        var key = "";

        for (
          var yearsAgo = yearsFromToday.fromBeginning;
          yearsAgo >= yearsFromToday.fromEnd && yearsAgo > 0;
          yearsAgo -= binSize
        ) {
          // The query logic here is: If the beginnning year is anytime before or
          //  during the last year of the bin AND the ending year is anytime after
          //  or during the first year of the bin, it counts.
          if (binSize == 1) {
            //Querying for just the current year needs to be treated a bit differently
            // and won't be caught in our for loop
            if (lastYear == today) {
              var oneYearFromNow = new Date(Date.UTC(today + 1, 0, 1));
              var now = new Date();

              facetQueries.push(
                "{!key=" +
                  lastYear +
                  "}(beginDate:[* TO " +
                  oneYearFromNow.toISOString() +
                  "/YEAR] AND endDate:[" +
                  now.toISOString() +
                  "/YEAR TO *])",
              );
            } else {
              key = today - yearsAgo;

              //The coverage should start sometime in this year range or earlier.
              var beginDateLimit = new Date(
                Date.UTC(today - (yearsAgo - 1), 0, 1),
              );
              //The coverage should end sometime in this year range or later.
              var endDateLimit = new Date(Date.UTC(today - yearsAgo, 0, 1));

              facetQueries.push(
                "{!key=" +
                  key +
                  "}(beginDate:[* TO " +
                  beginDateLimit.toISOString() +
                  "/YEAR] AND endDate:[" +
                  endDateLimit.toISOString() +
                  "/YEAR TO *])",
              );
            }
          }
          //If this is the last date range
          else if (yearsAgo <= binSize) {
            //Get the last year that will be included in this bin
            var firstYearInBin = today - yearsAgo,
              lastYearInBin = lastYear;

            //Label the facet query with a key for easier parsing
            // Because this is the last year range, which could be uneven with the other year ranges, use the exact end year
            key = firstYearInBin + "-" + lastYearInBin;

            //The coverage should start sometime in this year range or earlier.
            // Because this is the last year range, which could be uneven with the other year ranges, use the exact end year
            var beginDateLimit = new Date(
              Date.UTC(lastYearInBin, 11, 31, 23, 59, 59, 999),
            );
            //The coverage should end sometime in this year range or later.
            var endDateLimit = new Date(Date.UTC(firstYearInBin, 0, 1));

            facetQueries.push(
              "{!key=" +
                key +
                "}(beginDate:[* TO " +
                beginDateLimit.toISOString() +
                "/YEAR] AND endDate:[" +
                endDateLimit.toISOString() +
                "/YEAR TO *])",
            );
          }
          //For all other bins,
          else {
            //Get the last year that will be included in this bin
            var firstYearInBin = today - yearsAgo,
              lastYearInBin = today - yearsAgo + binSize - 1;

            //Label the facet query with a key for easier parsing
            key = firstYearInBin + "-" + lastYearInBin;

            //The coverage should start sometime in this year range or earlier.
            //  var beginDateLimit = new Date(Date.UTC(today - (yearsAgo - binSize), 0, 1));
            var beginDateLimit = new Date(
              Date.UTC(lastYearInBin, 11, 31, 23, 59, 59, 999),
            );
            //The coverage should end sometime in this year range or later.
            var endDateLimit = new Date(Date.UTC(firstYearInBin, 0, 1));

            facetQueries.push(
              "{!key=" +
                key +
                "}(beginDate:[* TO " +
                beginDateLimit.toISOString() +
                "/YEAR] AND endDate:[" +
                endDateLimit.toISOString() +
                "/YEAR TO *])",
            );
          }
        }

        var model = this;
        var successCallback = function (data, textStatus, xhr) {
          if (!data || !data.response || !data.response.numFound) {
            //Store falsey data
            model.set("totalCount", 0);
            model.trigger("change:totalCount");
            model.set("metadataCount", 0);
            model.trigger("change:metadataCount");
            model.set("metadataFormatIDs", ["", 0]);
            model.set("firstUpdate", null);
            model.set("metadataUpdateDates", []);
            model.set("temporalCoverage", 0);
            model.trigger("change:temporalCoverage");
          } else {
            //Save tthe number of metadata docs found
            model.set("metadataCount", data.response.numFound);
            model.set(
              "totalCount",
              model.get("dataCount") + data.response.numFound,
            );

            //Save the format ID facet counts
            if (
              data.facet_counts &&
              data.facet_counts.facet_fields &&
              data.facet_counts.facet_fields.formatId
            ) {
              model.set(
                "metadataFormatIDs",
                data.facet_counts.facet_fields.formatId,
              );
            } else {
              model.set("metadataFormatIDs", ["", 0]);
            }

            //Save the metadata update date counts
            if (
              data.facet_counts &&
              data.facet_counts.facet_ranges &&
              data.facet_counts.facet_ranges.dateUploaded
            ) {
              //Find the index of the first update date
              var updateFacets =
                  data.facet_counts.facet_ranges.dateUploaded.counts,
                cropAt = 0;

              for (var i = 1; i < updateFacets.length; i += 2) {
                //If there was at least one update/upload in this date range, then save this as the first update
                if (typeof updateFacets[i] == "number" && updateFacets[i] > 0) {
                  //Save the first first update date
                  cropAt = i;
                  model.set("firstUpdate", updateFacets[i - 1]);
                  //Save the update dates, but crop out months that are empty
                  model.set(
                    "metadataUpdateDates",
                    updateFacets.slice(cropAt + 1),
                  );
                  i = updateFacets.length;
                }
              }

              //If no update dates were found, save falsey values
              if (cropAt === 0) {
                model.set("firstUpdate", null);
                model.set("metadataUpdateDates", []);
              }
            }

            //Save the temporal coverage dates
            if (data.facet_counts && data.facet_counts.facet_queries) {
              //Find the beginDate and facets so we can store the earliest beginDate
              if (
                data.facet_counts.facet_fields &&
                data.facet_counts.facet_fields.beginDate
              ) {
                var earliestBeginDate = _.find(
                  data.facet_counts.facet_fields.beginDate,
                  function (value) {
                    return (
                      typeof value == "string" &&
                      parseInt(value.substring(0, 4)) > 1000
                    );
                  },
                );
                if (earliestBeginDate) {
                  model.set("firstBeginDate", earliestBeginDate);
                }
              }

              //Find the endDate and facets so we can store the latest endDate
              if (
                data.facet_counts.facet_fields &&
                data.facet_counts.facet_fields.endDate
              ) {
                var latestEndDate,
                  endDates = data.facet_counts.facet_fields.endDate,
                  nextYear = new Date().getUTCFullYear() + 1,
                  i = 0;

                //Iterate over each endDate and find the first valid one. (After year 1000 but not after today)
                while (!latestEndDate && i < endDates.length) {
                  var endDate = endDates[i];
                  if (typeof endDate == "string") {
                    endDate = parseInt(endDate.substring(0, 3));
                    if (endDate > 1000 && endDate < nextYear) {
                      latestEndDate = endDate;
                    }
                  }
                  i++;
                }

                //Save the latest endDate if one was found
                if (latestEndDate) {
                  model.set("lastEndDate", latestEndDate);
                }
              }

              //Save the temporal coverage year ranges
              var tempCoverages = data.facet_counts.facet_queries;
              model.set("temporalCoverage", tempCoverages);
            }

            //Get the total size of all the files in the index
            if (
              data.stats &&
              data.stats.stats_fields &&
              data.stats.stats_fields.size &&
              data.stats.stats_fields.size.sum
            ) {
              //Save the size sum
              model.set("metadataTotalSize", data.stats.stats_fields.size.sum);
              //If there is a data size sum,
              if (typeof model.get("dataTotalSize") == "number") {
                //Add it to the metadata size sum as the total sum
                model.set(
                  "totalSize",
                  model.get("dataTotalSize") + data.stats.stats_fields.size.sum,
                );
              }
            }
          }
        };

        //Construct the full URL for the query
        var fullQueryURL =
          MetacatUI.appModel.get("queryServiceUrl") +
          "q=" +
          query +
          "&fq=" +
          filterQuery +
          "&stats=" +
          stats +
          "&stats.field=" +
          statsField +
          "&facet=" +
          facet +
          "&facet.field=" +
          facetFormatIdField +
          "&facet.field=" +
          facetBeginDateField +
          "&facet.field=" +
          facetEndDateField +
          "&f." +
          facetFormatIdField +
          ".facet.mincount=" +
          facetFormatIdMin +
          "&f." +
          facetFormatIdField +
          ".facet.missing=" +
          facetFormatIdMissing +
          "&f." +
          facetBeginDateField +
          ".facet.mincount=" +
          facetDateMin +
          "&f." +
          facetEndDateField +
          ".facet.mincount=" +
          facetDateMin +
          "&f." +
          facetBeginDateField +
          ".facet.missing=" +
          facetDateMissing +
          "&f." +
          facetEndDateField +
          ".facet.missing=" +
          facetDateMissing +
          "&facet.limit=" +
          facetLimit +
          "&f." +
          facetRange +
          ".facet.missing=" +
          facetMissing +
          "&facet.range=" +
          facetRange +
          "&facet.range.start=" +
          facetRangeStart +
          "&facet.range.end=" +
          facetRangeEnd +
          "&facet.range.gap=" +
          encodeURIComponent(facetRangeGap) +
          "&facet.query=" +
          facetQueries.join("&facet.query=") +
          "&rows=" +
          rows +
          "&wt=" +
          wt;

        if (this.getRequestType(fullQueryURL) == "POST") {
          if (this.get("postQuery")) {
            query = this.get("postQuery");
          } else if (this.get("searchModel")) {
            query = this.get("searchModel").getQuery(undefined, {
              forPOST: true,
            });
            this.set("postQuery", query);
          }

          var queryData = new FormData();
          queryData.append("q", decodeURIComponent(query));
          queryData.append("fq", filterQuery);
          queryData.append("stats", stats);
          queryData.append("stats.field", statsField);
          queryData.append("facet", facet);
          queryData.append("facet.field", facetFormatIdField);
          queryData.append("facet.field", facetBeginDateField);
          queryData.append("facet.field", facetEndDateField);
          queryData.append(
            "f." + facetFormatIdField + ".facet.mincount",
            facetFormatIdMin,
          );
          queryData.append(
            "f." + facetFormatIdField + ".facet.missing",
            facetFormatIdMissing,
          );
          queryData.append(
            "f." + facetBeginDateField + ".facet.mincount",
            facetDateMin,
          );
          queryData.append(
            "f." + facetEndDateField + ".facet.mincount",
            facetDateMin,
          );
          queryData.append(
            "f." + facetBeginDateField + ".facet.missing",
            facetDateMissing,
          );
          queryData.append(
            "f." + facetEndDateField + ".facet.missing",
            facetDateMissing,
          );
          queryData.append("facet.limit", facetLimit);
          queryData.append("facet.range", facetRange);
          queryData.append("facet.range.start", facetRangeStart);
          queryData.append("facet.range.end", facetRangeEnd);
          queryData.append("facet.range.gap", facetRangeGap);
          queryData.append("f." + facetRange + ".facet.missing", facetMissing);
          queryData.append("rows", rows);
          queryData.append("wt", wt);

          //Add the facet queries to the POST body
          _.each(facetQueries, function (facetQuery) {
            queryData.append("facet.query", facetQuery);
          });

          //Create the request settings for POST requests
          var requestSettings = {
            url: MetacatUI.appModel.get("queryServiceUrl"),
            type: "POST",
            contentType: false,
            processData: false,
            data: queryData,
            dataType: "json",
            success: successCallback,
          };
        } else {
          //Create the request settings for GET requests
          var requestSettings = {
            url: fullQueryURL,
            type: "GET",
            dataType: "json",
            success: successCallback,
          };
        }

        //Send the request
        $.ajax(
          _.extend(
            requestSettings,
            MetacatUI.appUserModel.createAjaxSettings(),
          ),
        );
      },

      /**
       * Queries for statistics about data objects
       */
      getDataStats: function () {
        //Get the query string from this model
        var query = this.get("query") || "";
        //If there is a query set on the model, do a join on the resourceMap field
        if (
          query.trim() !== "*:*" &&
          query.trim().length > 0 &&
          !this.get("isSystemMetadataQuery") &&
          MetacatUI.appModel.get("enableSolrJoins")
        ) {
          query = "{!join from=resourceMap to=resourceMap}" + query;
        }

        //Filter out resource maps and metatdata objects
        var filterQuery = "formatType:DATA AND -obsoletedBy:*",
          //Use the stats feature to get the sum of the file size
          stats = "true",
          statsField = "size",
          //Get the facet counts for formatIds
          facet = "true",
          facetField = "formatId",
          facetFormatIdMin = "1",
          facetFormatIdMissing = "false",
          facetLimit = "-1",
          //Get the upload counts for each month
          facetRange = "dateUploaded",
          facetRangeGap = "+1MONTH",
          facetRangeStart = "1900-01-01T00:00:00.000Z",
          facetRangeEnd = new Date().toISOString(),
          facetRangeMissing = "true",
          //Don't return any result docs
          rows = "0",
          //Use JSON for the response format
          wt = "json";

        var fullQueryURL =
          MetacatUI.appModel.get("queryServiceUrl") +
          "q=" +
          query +
          "&fq=" +
          filterQuery +
          "&stats=" +
          stats +
          "&stats.field=" +
          statsField +
          "&facet=" +
          facet +
          "&facet.field=" +
          facetField +
          "&facet.limit=" +
          facetLimit +
          "&f." +
          facetField +
          ".facet.mincount=" +
          facetFormatIdMin +
          "&f." +
          facetField +
          ".facet.missing=" +
          facetFormatIdMissing +
          "&f." +
          facetRange +
          ".facet.missing=" +
          facetRangeMissing +
          "&facet.range=" +
          facetRange +
          "&facet.range.start=" +
          facetRangeStart +
          "&facet.range.end=" +
          facetRangeEnd +
          "&facet.range.gap=" +
          encodeURIComponent(facetRangeGap) +
          "&rows=" +
          rows +
          "&wt=" +
          wt;

        var model = this;
        var successCallback = function (data, textStatus, xhr) {
          if (!data || !data.response || !data.response.numFound) {
            //Store falsey data
            model.set("dataCount", 0);
            model.trigger("change:dataCount");
            model.set("dataFormatIDs", ["", 0]);
            model.set("dataUpdateDates", []);
            model.set("dataTotalSize", 0);

            if (typeof model.get("metadataTotalSize") == "number") {
              //Use the metadata total size as the total size
              model.set("totalSize", model.get("metadataTotalSize"));
            }
          } else {
            //Save the number of data docs found
            model.set("dataCount", data.response.numFound);
            model.set(
              "totalCount",
              model.get("metadataCount") + data.response.numFound,
            );

            //Save the format ID facet counts
            if (
              data.facet_counts &&
              data.facet_counts.facet_fields &&
              data.facet_counts.facet_fields.formatId
            ) {
              model.set(
                "dataFormatIDs",
                data.facet_counts.facet_fields.formatId,
              );
            } else {
              model.set("dataFormatIDs", ["", 0]);
            }

            //Save the data update date counts
            if (
              data.facet_counts &&
              data.facet_counts.facet_ranges &&
              data.facet_counts.facet_ranges.dateUploaded
            ) {
              //Find the index of the first update date
              var updateFacets =
                  data.facet_counts.facet_ranges.dateUploaded.counts,
                cropAt = 0;

              for (var i = 1; i < updateFacets.length; i += 2) {
                //If there was at least one update/upload in this date range, then save this as the first update
                if (typeof updateFacets[i] == "number" && updateFacets[i] > 0) {
                  //Save the first first update date
                  cropAt = i;
                  model.set("firstUpdate", updateFacets[i - 1]);
                  //Save the update dates, but crop out months that are empty
                  model.set("dataUpdateDates", updateFacets.slice(cropAt + 1));
                  i = updateFacets.length;
                }
              }

              //If no update dates were found, save falsey values
              if (cropAt === 0) {
                model.set("firstUpdate", null);
                model.set("dataUpdateDates", []);
              }
            }

            //Get the total size of all the files in the index
            if (
              data.stats &&
              data.stats.stats_fields &&
              data.stats.stats_fields.size &&
              data.stats.stats_fields.size.sum
            ) {
              //Save the size sum
              model.set("dataTotalSize", data.stats.stats_fields.size.sum);
              //If there is a metadata size sum,
              if (model.get("metadataTotalSize") > 0) {
                //Add it to the data size sum as the total sum
                model.set(
                  "totalSize",
                  model.get("metadataTotalSize") +
                    data.stats.stats_fields.size.sum,
                );
              }
            }
          }
        };

        if (this.getRequestType(fullQueryURL) == "POST") {
          if (this.get("postQuery")) {
            query = this.get("postQuery");
          } else if (this.get("searchModel")) {
            query = this.get("searchModel").getQuery(undefined, {
              forPOST: true,
            });
            this.set("postQuery", query);
          }

          var queryData = new FormData();
          queryData.append("q", decodeURIComponent(query));
          queryData.append("fq", filterQuery);
          queryData.append("stats", stats);
          queryData.append("stats.field", statsField);
          queryData.append("facet", facet);
          queryData.append("facet.field", facetField);
          queryData.append("facet.limit", facetLimit);
          queryData.append(
            "f." + facetField + ".facet.mincount",
            facetFormatIdMin,
          );
          queryData.append(
            "f." + facetField + ".facet.missing",
            facetFormatIdMissing,
          );
          queryData.append(
            "f." + facetRange + ".facet.missing",
            facetRangeMissing,
          );
          queryData.append("facet.range", facetRange);
          queryData.append("facet.range.start", facetRangeStart);
          queryData.append("facet.range.end", facetRangeEnd);
          queryData.append("facet.range.gap", facetRangeGap);
          queryData.append("rows", rows);
          queryData.append("wt", wt);

          //Create the request settings for POST requests
          var requestSettings = {
            url: MetacatUI.appModel.get("queryServiceUrl"),
            type: "POST",
            contentType: false,
            processData: false,
            data: queryData,
            dataType: "json",
            success: successCallback,
          };
        } else {
          //Create the request settings for GET requests
          var requestSettings = {
            url: fullQueryURL,
            type: "GET",
            dataType: "json",
            success: successCallback,
          };
        }

        //Send the request
        $.ajax(
          _.extend(
            requestSettings,
            MetacatUI.appUserModel.createAjaxSettings(),
          ),
        );
      },

      /**
       * Retrieves an image of the metadata assessment scores
       */
      getMdqScores: function () {
        try {
          var myImage = new Image();
          var model = this;
          myImage.crossOrigin = ""; // or "anonymous"

          // Call the function with the URL we want to load, but then chain the
          // promise then() method on to the end of it. This contains two callbacks
          var serviceUrl = MetacatUI.appModel.get("mdqScoresServiceUrl");

          if (!serviceUrl) {
            this.set("mdqScoresImage", this.defaults().mdqScoresImage);
            this.trigger("change:mdqScoresImage");
            return;
          }

          if (
            Array.isArray(MetacatUI.appModel.get("mdqAggregatedSuiteIds")) &&
            MetacatUI.appModel.get("mdqAggregatedSuiteIds").length
          ) {
            var suite = MetacatUI.appModel.get("mdqAggregatedSuiteIds")[0];

            var id;

            if (
              this.get("mdqImageId") &&
              typeof this.get("mdqImageId") == "string"
            ) {
              id = this.get("mdqImageId");
            } else if (MetacatUI.appView.currentView) {
              id = MetacatUI.appView.currentView.model.get("seriesId");
            }

            //If no ID was found, exit without getting the image
            if (!id) {
              return;
            }

            var url = serviceUrl + "?id=" + id + "&suite=" + suite;

            this.imgLoad(url).then(
              function (response) {
                // The first runs when the promise resolves, with the request.reponse specified within the resolve() method.
                var imageURL = window.URL.createObjectURL(response);
                myImage.src = imageURL;
                model.set("mdqScoresImage", myImage);
                // The second runs when the promise is rejected, and logs the Error specified with the reject() method.
              },
              function (Error) {
                console.error(Error);
              },
            );
          } else {
            this.set("mdqScoresImage", this.defaults().mdqScoresImage);
          }
        } catch (e) {
          this.set("mdqScoresImage", this.defaults().mdqScoresImage);
          this.trigger("change:mdqScoresImage");
          console.error("Cannot get the Metadata Assessment scores: ", e);
        }
      },

      /**
       * Retrieves an image via a Promise. Primarily used by {@link Stats#getMdqScores}
       * @param {string} url - The URL of the image
       */
      imgLoad: function (url) {
        // Create new promise with the Promise() constructor;
        // This has as its argument a function with two parameters, resolve and reject
        var model = this;
        return new Promise(function (resolve, reject) {
          // Standard XHR to load an image
          var request = new XMLHttpRequest();
          request.open("GET", url);
          request.responseType = "blob";

          // When the request loads, check whether it was successful
          request.onload = function () {
            if (request.status === 200) {
              // If successful, resolve the promise by passing back the request response
              resolve(request.response);
            } else {
              // If it fails, reject the promise with a error message
              reject(
                new Error(
                  "Image didn't load successfully; error code:" +
                    request.statusText,
                ),
              );
              model.set("mdqScoresError", request.statusText);
            }
          };

          request.onerror = function () {
            console.log("onerror");
            // Also deal with the case when the entire request fails to begin with
            // This is probably a network error, so reject the promise with an appropriate message
            reject(new Error("There was a network error."));
          };

          // Send the request
          request.send();
        });
      },

      /**
       * Sends a Solr query to get the earliest beginDate. If there are no beginDates in the index, then it
       * searches for the earliest endDate.
       */
      getFirstBeginDate: function () {
        var model = this;

        //Define a success callback when the query is successful
        var successCallback = function (data, textStatus, xhr) {
          //If nothing was found...
          if (!data || !data.response || !data.response.numFound) {
            //Construct a query to find the earliest endDate
            var query =
                model.get("query") +
                " AND endDate:[" +
                model.get("firstPossibleDate") +
                " TO " +
                new Date().toISOString() +
                "]" + //Use date filter to weed out badly formatted data
                " AND -obsoletedBy:*",
              //Get one row only
              rows = "1",
              //Sort the results in ascending order
              sort = "endDate asc",
              //Return only the endDate field
              fl = "endDate";

            var successCallback = function (endDateData, textStatus, xhr) {
              //If not endDates or beginDates are found, there is no temporal data in the index, so save falsey values
              if (
                !endDateData ||
                !endDateData.response ||
                !endDateData.response.numFound
              ) {
                model.set("firstBeginDate", null);
                model.set("lastEndDate", null);
              } else {
                model.set(
                  "firstBeginDate",
                  new Date(endDateData.response.docs[0].endDate),
                );
              }
            };

            if (model.get("usePOST")) {
              var queryData = new FormData();
              queryData.append("q", decodeURIComponent(query));
              queryData.append("rows", rows);
              queryData.append("sort", sort);
              queryData.append("fl", fl);
              queryData.append("wt", "json");

              var requestSettings = {
                url: MetacatUI.appModel.get("queryServiceUrl"),
                type: "POST",
                contentType: false,
                processData: false,
                data: queryData,
                dataType: "json",
                success: successCallback,
              };
            } else {
              //Find the earliest endDate if there are no beginDates
              var requestSettings = {
                url:
                  MetacatUI.appModel.get("queryServiceUrl") +
                  "q=" +
                  query +
                  "&rows=" +
                  rows +
                  "&sort=" +
                  sort +
                  "&fl=" +
                  fl +
                  "&wt=json",
                type: "GET",
                dataType: "json",
                success: successCallback,
              };
            }

            $.ajax(
              _.extend(
                requestSettings,
                MetacatUI.appUserModel.createAjaxSettings(),
              ),
            );
          } else {
            // Save the earliest beginDate
            model.set(
              "firstBeginDate",
              new Date(data.response.docs[0].beginDate),
            );
            model.trigger("change:firstBeginDate");
          }
        };

        //Construct a query
        var specialQueryParams =
            " AND beginDate:[" +
            this.get("firstPossibleDate") +
            " TO " +
            new Date().toISOString() +
            "] AND -obsoletedBy:* AND -formatId:*dataone.org/collections* AND -formatId:*dataone.org/portals*",
          query = this.get("query") + specialQueryParams,
          //Get one row only
          rows = "1",
          //Sort the results in ascending order
          sort = "beginDate asc",
          //Return only the beginDate field
          fl = "beginDate";

        if (this.get("usePOST")) {
          //Get the unencoded query string
          if (this.get("postQuery")) {
            query = this.get("postQuery") + specialQueryParams;
          } else if (this.get("searchModel")) {
            query = this.get("searchModel").getQuery(undefined, {
              forPOST: true,
            });
            this.set("postQuery", query);
            query = query + specialQueryParams;
          }

          var queryData = new FormData();
          queryData.append("q", decodeURIComponent(query));
          queryData.append("rows", rows);
          queryData.append("sort", sort);
          queryData.append("fl", fl);
          queryData.append("wt", "json");

          var requestSettings = {
            url: MetacatUI.appModel.get("queryServiceUrl"),
            type: "POST",
            contentType: false,
            processData: false,
            data: queryData,
            dataType: "json",
            success: successCallback,
          };
        } else {
          var requestSettings = {
            url:
              MetacatUI.appModel.get("queryServiceUrl") +
              "q=" +
              query +
              "&rows=" +
              rows +
              "&fl=" +
              fl +
              "&sort=" +
              sort +
              "&wt=json",
            type: "GET",
            dataType: "json",
            success: successCallback,
          };
        }

        //Send the query
        $.ajax(
          _.extend(
            requestSettings,
            MetacatUI.appUserModel.createAjaxSettings(),
          ),
        );
      },

      // Getting total number of replicas for repository profiles
      getTotalReplicas: function (memberNodeID) {
        var model = this;

        var requestSettings = {
          url:
            MetacatUI.appModel.get("queryServiceUrl") +
            "q=replicaMN:" +
            memberNodeID +
            " AND  -datasource:" +
            memberNodeID +
            " AND formatType:METADATA" +
            " AND -obsoletedBy:*" +
            " &wt=json&rows=0",
          type: "GET",
          dataType: "json",
          success: function (data, textStatus, xhr) {
            model.set("totalReplicas", data.response.numFound);
          },
          error: function (data, textStatus, xhr) {
            model.set("totalReplicas", 0);
          },
        };

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

      /**
       * Gets the latest endDate from the Solr index
       */
      getLastEndDate: function () {
        var model = this;

        var now = new Date();

        //Get the latest temporal data coverage year
        var specialQueryParams =
            " AND endDate:[" +
            this.get("firstPossibleDate") +
            " TO " +
            now.toISOString() +
            "]" + //Use date filter to weed out badly formatted data
            " AND -obsoletedBy:* AND -formatId:*dataone.org/collections* AND -formatId:*dataone.org/portals*",
          query = this.get("query") + specialQueryParams,
          rows = 1,
          fl = "endDate",
          sort = "endDate desc",
          wt = "json";

        var successCallback = function (data, textStatus, xhr) {
          if (typeof data == "string") {
            data = JSON.parse(data);
          }

          if (!data || !data.response || !data.response.numFound) {
            //Save some falsey values if none are found
            model.set("lastEndDate", null);
          } else {
            // Save the earliest beginDate and total found in our model - but do not accept a year greater than this current year
            var now = new Date();
            if (
              new Date(data.response.docs[0].endDate).getUTCFullYear() >
              now.getUTCFullYear()
            ) {
              model.set("lastEndDate", now);
            } else {
              model.set("lastEndDate", new Date(data.response.docs[0].endDate));
            }

            model.trigger("change:lastEndDate");
          }
        };

        if (this.get("usePOST")) {
          //Get the unencoded query string
          if (this.get("postQuery")) {
            query = this.get("postQuery") + specialQueryParams;
          } else if (this.get("searchModel")) {
            query = this.get("searchModel").getQuery(undefined, {
              forPOST: true,
            });
            this.set("postQuery", query);
            query = query + specialQueryParams;
          }

          var queryData = new FormData();
          queryData.append("q", decodeURIComponent(query));
          queryData.append("rows", rows);
          queryData.append("sort", sort);
          queryData.append("fl", fl);
          queryData.append("wt", "json");

          var requestSettings = {
            url: MetacatUI.appModel.get("queryServiceUrl"),
            type: "POST",
            contentType: false,
            processData: false,
            data: queryData,
            dataType: "json",
            success: successCallback,
          };
        } else {
          //Query for the latest endDate
          var requestSettings = {
            url:
              MetacatUI.appModel.get("queryServiceUrl") +
              "q=" +
              query +
              "&rows=" +
              rows +
              "&fl=" +
              fl +
              "&sort=" +
              sort +
              "&wt=" +
              wt,
            type: "GET",
            dataType: "json",
            success: successCallback,
          };
        }

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

      /**
       * Given the query or URL, determine whether this model should send GET or POST
       * requests, because of URL length restrictions in browsers.
       * @param {string} queryOrURLString - The full query or URL that will be sent to the query service
       * @returns {string} The request type to use. Either `GET` or `POST`
       */
      getRequestType: function (queryOrURLString) {
        //If POSTs to the query service are disabled completely, use GET
        if (MetacatUI.appModel.get("disableQueryPOSTs")) {
          return "GET";
        }
        //If POSTs are enabled and the URL is over the maximum, use POST
        else if (
          queryOrURLString &&
          queryOrURLString.length > this.get("maxQueryLength")
        ) {
          return "POST";
        }
        //Otherwise, default to GET
        else {
          return "GET";
        }
      },

      /**
       * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} and {@link Stats#getDataStats} to get the formatTypes.
       * This function may be removed in a future release.
       */
      getFormatTypes: function () {
        this.getMetadataStats();
        this.getDataStats();
      },

      /**
       * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getDataStats} to get the formatTypes.
       * This function may be removed in a future release.
       */
      getDataFormatIDs: function () {
        this.getDataStats();
      },

      /**
       * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} to get the formatTypes.
       * This function may be removed in a future release.
       */
      getMetadataFormatIDs: function () {
        this.getMetadataStats();
      },

      /**
       * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} and {@link Stats#getDataStats} to get the formatTypes.
       * This function may be removed in a future release.
       */
      getUpdateDates: function () {
        this.getMetadataStats();
        this.getDataStats();
      },

      /**
       * @deprecated as of MetacatUI version 2.12.0. Use {@link Stats#getMetadataStats} to get the formatTypes.
       * This function may be removed in a future release.
       */
      getCollectionYearFacets: function () {
        this.getMetadataStats();
      },
    },
  );
  return Stats;
});