Source: src/js/views/MetadataView.js

define([
  "jquery",
  "jqueryui",
  "underscore",
  "backbone",
  "gmaps",
  "fancybox",
  "clipboard",
  "collections/DataPackage",
  "models/DataONEObject",
  "models/PackageModel",
  "models/SolrResult",
  "models/metadata/ScienceMetadata",
  "models/MetricsModel",
  "common/Utilities",
  "views/DataPackageView",
  "views/DownloadButtonView",
  "views/ProvChartView",
  "views/MetadataIndexView",
  "views/ExpandCollapseListView",
  "views/ProvStatementView",
  "views/CitationHeaderView",
  "views/citations/CitationModalView",
  "views/AnnotationView",
  "views/MarkdownView",
  "text!templates/metadata/metadata.html",
  "text!templates/dataSource.html",
  "text!templates/publishDOI.html",
  "text!templates/newerVersion.html",
  "text!templates/loading.html",
  "text!templates/metadataControls.html",
  "text!templates/metadataInfoIcons.html",
  "text!templates/alert.html",
  "text!templates/editMetadata.html",
  "text!templates/dataDisplay.html",
  "text!templates/map.html",
  "text!templates/annotation.html",
  "text!templates/metaTagsHighwirePress.html",
  "uuid",
  "views/MetricView",
], function (
  $,
  $ui,
  _,
  Backbone,
  gmaps,
  fancybox,
  Clipboard,
  DataPackage,
  DataONEObject,
  Package,
  SolrResult,
  ScienceMetadata,
  MetricsModel,
  Utilities,
  DataPackageView,
  DownloadButtonView,
  ProvChart,
  MetadataIndex,
  ExpandCollapseList,
  ProvStatement,
  CitationHeaderView,
  CitationModalView,
  AnnotationView,
  MarkdownView,
  MetadataTemplate,
  DataSourceTemplate,
  PublishDoiTemplate,
  VersionTemplate,
  LoadingTemplate,
  ControlsTemplate,
  MetadataInfoIconsTemplate,
  AlertTemplate,
  EditMetadataTemplate,
  DataDisplayTemplate,
  MapTemplate,
  AnnotationTemplate,
  metaTagsHighwirePressTemplate,
  uuid,
  MetricView,
) {
  "use strict";

  /**
   * @class MetadataView
   * @classdesc A human-readable view of a science metadata file
   * @classcategory Views
   * @extends Backbone.View
   * @constructor
   * @screenshot views/MetadataView.png
   */
  var MetadataView = Backbone.View.extend(
    /** @lends MetadataView.prototype */ {
      subviews: [],

      pid: null,
      seriesId: null,
      saveProvPending: false,

      model: new SolrResult(),
      packageModels: new Array(),
      entities: new Array(),
      dataPackage: null,
      dataPackageSynced: false,
      el: "#Content",
      metadataContainer: "#metadata-container",
      citationContainer: "#citation-container",
      tableContainer: "#table-container",
      controlsContainer: "#metadata-controls-container",
      metricsContainer: "#metrics-controls-container",
      editorControlsContainer: "#editor-controls-container",
      breadcrumbContainer: "#breadcrumb-container",
      parentLinkContainer: "#parent-link-container",
      dataSourceContainer: "#data-source-container",
      articleContainer: "#article-container",

      type: "Metadata",

      //Templates
      template: _.template(MetadataTemplate),
      alertTemplate: _.template(AlertTemplate),
      doiTemplate: _.template(PublishDoiTemplate),
      versionTemplate: _.template(VersionTemplate),
      loadingTemplate: _.template(LoadingTemplate),
      controlsTemplate: _.template(ControlsTemplate),
      infoIconsTemplate: _.template(MetadataInfoIconsTemplate),
      dataSourceTemplate: _.template(DataSourceTemplate),
      editMetadataTemplate: _.template(EditMetadataTemplate),
      dataDisplayTemplate: _.template(DataDisplayTemplate),
      mapTemplate: _.template(MapTemplate),
      metaTagsHighwirePressTemplate: _.template(metaTagsHighwirePressTemplate),

      objectIds: [],

      /**
       * Text to display in the help tooltip for the alternative identifier field,
       * if the field is present.
       * @type {string}
       * @since 2.26.0
       */
      alternativeIdentifierHelpText: `
         An identifier used to reference this dataset in the past or in another
         system. This could be a link to the original dataset or an old
         identifier that was replaced. The referenced dataset may be the same
         or different from the one you are currently viewing, and its
         accessibility may vary. It may provide additional context about the
         history and evolution of the dataset.
        `,

      // Delegated events for creating new items, and clearing completed ones.
      events: {
        "click #publish": "publish",
        "mouseover .highlight-node": "highlightNode",
        "mouseout  .highlight-node": "highlightNode",
        "click     .preview": "previewData",
        "click     #save-metadata-prov": "saveProv",
      },

      initialize: function (options) {
        if (options === undefined || !options) var options = {};

        this.pid =
          options.pid || options.id || MetacatUI.appModel.get("pid") || null;

        this.dataPackage = null;

        if (typeof options.el !== "undefined") this.setElement(options.el);
      },

      // Render the main metadata view
      render: function () {
        this.stopListening();

        MetacatUI.appModel.set("headerType", "default");
        //  this.showLoading("Loading...");

        //Reset various properties of this view first
        this.classMap = new Array();
        this.subviews = new Array();
        this.model.set(this.model.defaults);
        this.packageModels = new Array();

        // get the pid to render
        if (!this.pid) this.pid = MetacatUI.appModel.get("pid");

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

        //Listen to when the metadata has been rendered
        this.once("metadataLoaded", function () {
          this.createAnnotationViews();
          this.insertMarkdownViews();
        });

        //Listen to when the package table has been rendered
        this.once("dataPackageRendered", function () {
          var packageTableContainer = this.$("#data-package-container");
          $(packageTableContainer).children(".loading").remove();

          //Scroll to the element on the page that is in the hash fragment (if there is one)
          this.scrollToFragment();
        });

        this.getModel();

        return this;
      },

      /**
       * Retrieve the resource map given its PID, and when it's fetched,
       * check for write permissions, then check for private members in the package
       * table view, if there is one.
       * @param {string} pid - The PID of the resource map
       */
      getDataPackage: function (pid) {
        //Create a DataONEObject model to use in the DataPackage collection.
        var dataOneObject = new ScienceMetadata({ id: this.model.get("id") });

        var view = this;

        // Create a new data package with this id
        this.dataPackage = new DataPackage([dataOneObject], { id: pid });

        this.dataPackage.mergeModels([this.model]);

        // If there is no resource map
        if (!pid) {
          // mark the data package as synced,
          // since there are no other models to fetch
          this.dataPackageSynced = true;
          this.trigger("changed:dataPackageSynced");
          this.checkWritePermissions();
          return;
        }

        this.listenToOnce(this.dataPackage, "complete", function () {
          this.dataPackageSynced = true;
          this.trigger("changed:dataPackageSynced");
          var dataPackageView = _.findWhere(this.subviews, {
            type: "DataPackage",
          });
          if (dataPackageView) {
            dataPackageView.dataPackageCollection = this.dataPackage;
            dataPackageView.checkForPrivateMembers();
          }
        });

        this.listenToOnce(this.dataPackage, "fetchFailed", function () {
          view.dataPackageSynced = false;

          // stop listening to the fetch complete
          view.stopListening(view.dataPackage, "complete");

          //Remove the loading elements
          view.$(view.tableContainer).find(".loading").remove();

          //Show an error message
          MetacatUI.appView.showAlert(
            "Error retrieving files for this data package.",
            "alert-error",
            view.$(view.tableContainer),
          );
        });

        if (
          this.dataPackage.packageModel &&
          this.dataPackage.packageModel.get("synced") === true
        ) {
          this.checkWritePermissions();
        } else {
          this.listenToOnce(this.dataPackage.packageModel, "sync", function () {
            this.checkWritePermissions();
          });
        }
        // Fetch the data package. DataPackage.parse() triggers 'complete'
        this.dataPackage.fetch({
          fetchModels: false,
        });
      },

      /*
       * Retrieves information from the index about this object, given the id (passed from the URL)
       * When the object info is retrieved from the index, we set up models depending on the type of object this is
       */
      getModel: function (pid) {
        //Get the pid and sid
        if (typeof pid === "undefined" || !pid) var pid = this.pid;
        if (typeof this.seriesId !== "undefined" && this.seriesId)
          var sid = this.seriesId;

        //Get the package ID
        this.model.set({ id: pid, seriesId: sid });
        var model = this.model;

        this.listenToOnce(model, "sync", function () {
          if (
            this.model.get("formatType") == "METADATA" ||
            !this.model.get("formatType")
          ) {
            this.model = model;
            this.renderMetadata();
          } else if (this.model.get("formatType") == "DATA") {
            //Get the metadata pids that document this data object
            var isDocBy = this.model.get("isDocumentedBy");

            //If there is only one metadata pid that documents this data object, then
            // get that metadata model for this view.
            if (isDocBy && isDocBy.length == 1) {
              this.navigateWithFragment(_.first(isDocBy), this.pid);

              return;
            }
            //If more than one metadata doc documents this data object, it is most likely
            // multiple versions of the same metadata. So we need to find the latest version.
            else if (isDocBy && isDocBy.length > 1) {
              var view = this;

              require([
                "collections/Filters",
                "collections/SolrResults",
              ], function (Filters, SolrResults) {
                //Create a search for the metadata docs that document this data object
                var searchFilters = new Filters([
                    {
                      values: isDocBy,
                      fields: ["id", "seriesId"],
                      operator: "OR",
                      fieldsOperator: "OR",
                      matchSubstring: false,
                    },
                  ]),
                  //Create a list of search results
                  searchResults = new SolrResults([], {
                    rows: isDocBy.length,
                    query: searchFilters.getQuery(),
                    fields: "obsoletes,obsoletedBy,id",
                  });

                //When the search results are returned, process those results
                view.listenToOnce(
                  searchResults,
                  "sync",
                  function (searchResults) {
                    //Keep track of the latest version of the metadata doc(s)
                    var latestVersions = [];

                    //Iterate over each search result and find the latest version of each metadata version chain
                    searchResults.each(function (searchResult) {
                      //If this metadata isn't obsoleted by another object, it is the latest version
                      if (!searchResult.get("obsoletedBy")) {
                        latestVersions.push(searchResult.get("id"));
                      }
                      //If it is obsoleted by another object but that newer object does not document this data, then this is the latest version
                      else if (
                        !_.contains(isDocBy, searchResult.get("obsoletedBy"))
                      ) {
                        latestVersions.push(searchResult.get("id"));
                      }
                    }, view);

                    //If at least one latest version was found (should always be the case),
                    if (latestVersions.length) {
                      //Set that metadata pid as this view's pid and get that metadata model.
                      // TODO: Support navigation to multiple metadata docs. This should be a rare occurence, but
                      // it is possible that more than one metadata version chain documents a data object, and we need
                      // to show the user that the data is involved in multiple datasets.
                      view.navigateWithFragment(latestVersions[0], view.pid);
                    }
                    //If a latest version wasn't found, which should never happen, but just in case, default to the
                    // last metadata pid in the isDocumentedBy field (most liekly to be the most recent since it was indexed last).
                    else {
                      view.navigateWithFragment(_.last(isDocBy), view.pid);
                    }
                  },
                );

                //Send the query to the Solr search service
                searchResults.query();
              });

              return;
            } else {
              this.noMetadata(this.model);
            }
          } else if (this.model.get("formatType") == "RESOURCE") {
            var packageModel = new Package({ id: this.model.get("id") });
            packageModel.on(
              "complete",
              function () {
                var metadata = packageModel.getMetadata();

                if (!metadata) {
                  this.noMetadata(packageModel);
                } else {
                  this.model = metadata;
                  this.pid = this.model.get("id");
                  this.renderMetadata();
                  if (this.model.get("resourceMap"))
                    this.getPackageDetails(this.model.get("resourceMap"));
                }
              },
              this,
            );
            packageModel.getMembers();
            return;
          }

          //Get the package information
          this.getPackageDetails(model.get("resourceMap"));
        });

        //Listen to 404 and 401 errors when we get the metadata object
        this.listenToOnce(model, "404", this.showNotFound);
        this.listenToOnce(model, "401", this.showIsPrivate);

        //Fetch the model
        model.getInfo();
      },

      renderMetadata: function () {
        var pid = this.model.get("id");

        this.hideLoading();
        //Load the template which holds the basic structure of the view
        this.$el.html(this.template());
        this.$(this.tableContainer).html(
          this.loadingTemplate({
            msg: "Retrieving data set details...",
          }),
        );

        //Insert the breadcrumbs
        this.insertBreadcrumbs();
        //Insert the citation
        this.insertCitation();
        //Insert the data source logo
        this.insertDataSource();
        // is this the latest version? (includes DOI link when needed)
        this.showLatestVersion();

        // Insert various metadata controls in the page
        this.insertControls();

        // If we're displaying the metrics well then display copy citation and edit button
        // inside the well
        if (MetacatUI.appModel.get("displayDatasetMetrics")) {
          //Insert Metrics Stats into the dataset landing pages
          this.insertMetricsControls();
        }

        //Show loading icon in metadata section
        this.$(this.metadataContainer).html(
          this.loadingTemplate({ msg: "Retrieving metadata ..." }),
        );

        // Check for a view service in this MetacatUI.appModel
        if (
          MetacatUI.appModel.get("viewServiceUrl") !== undefined &&
          MetacatUI.appModel.get("viewServiceUrl")
        )
          var endpoint =
            MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(pid);

        if (endpoint && typeof endpoint !== "undefined") {
          var viewRef = this;
          var loadSettings = {
            url: endpoint,
            success: function (response, status, xhr) {
              try {
                //If the user has navigated away from the MetadataView, then don't render anything further
                if (MetacatUI.appView.currentView != viewRef) return;

                //Our fallback is to show the metadata details from the Solr index
                if (
                  status == "error" ||
                  !response ||
                  typeof response !== "string"
                )
                  viewRef.renderMetadataFromIndex();
                else {
                  //Check for a response that is a 200 OK status, but is an error msg
                  if (
                    response.length < 250 &&
                    response.indexOf("Error transforming document") > -1 &&
                    viewRef.model.get("indexed")
                  ) {
                    viewRef.renderMetadataFromIndex();
                    return;
                  }
                  //Mark this as a metadata doc with no stylesheet, or one that is at least different than usual EML and FGDC
                  else if (response.indexOf('id="Metadata"') == -1) {
                    viewRef.$el.addClass("container no-stylesheet");

                    if (viewRef.model.get("indexed")) {
                      viewRef.renderMetadataFromIndex();
                      return;
                    }
                  }

                  //Now show the response from the view service
                  viewRef.$(viewRef.metadataContainer).html(response);

                  viewRef.storeEntityPIDs(response);

                  //If there is no info from the index and there is no metadata doc rendered either, then display a message
                  if (
                    viewRef.$el.is(".no-stylesheet") &&
                    viewRef.model.get("archived") &&
                    !viewRef.model.get("indexed")
                  )
                    viewRef.$(viewRef.metadataContainer).prepend(
                      viewRef.alertTemplate({
                        msg: "There is limited metadata about this dataset since it has been archived.",
                      }),
                    );

                  viewRef.alterMarkup();

                  viewRef.trigger("metadataLoaded");

                  //Add a map of the spatial coverage
                  if (gmaps) viewRef.insertSpatialCoverageMap();

                  // Injects Clipboard objects into DOM elements returned from the View Service
                  viewRef.insertCopiables();
                }
              } catch (e) {
                console.log(
                  "Error rendering metadata from the view service",
                  e,
                );
                console.log("Response from the view service: ", response);
                viewRef.renderMetadataFromIndex();
              }
            },
            error: function (xhr, textStatus, errorThrown) {
              viewRef.renderMetadataFromIndex();
            },
          };

          $.ajax(
            _.extend(loadSettings, MetacatUI.appUserModel.createAjaxSettings()),
          );
        } else this.renderMetadataFromIndex();

        // Insert the Linked Data into the header of the page.
        if (MetacatUI.appModel.get("isJSONLDEnabled")) {
          var json = this.generateJSONLD();
          this.insertJSONLD(json);
        }

        this.insertCitationMetaTags();
      },

      /* If there is no view service available, then display the metadata fields from the index */
      renderMetadataFromIndex: function () {
        var metadataFromIndex = new MetadataIndex({
          pid: this.pid,
          parentView: this,
        });
        this.subviews.push(metadataFromIndex);

        //Add the metadata HTML
        this.$(this.metadataContainer).html(metadataFromIndex.render().el);

        var view = this;

        this.listenTo(metadataFromIndex, "complete", function () {
          //Add the package contents
          view.insertPackageDetails();

          //Add a map of the spatial coverage
          if (gmaps) view.insertSpatialCoverageMap();
        });
      },

      removeCitation: function () {
        var citation = "",
          citationEl = null;

        //Find the citation element
        if (this.$(".citation").length > 0) {
          //Get the text for the citation
          citation = this.$(".citation").text();

          //Save this element in the view
          citationEl = this.$(".citation");
        }
        //Older versions of Metacat (v2.4.3 and older) will not have the citation class in the XSLT. Find the citation another way
        else {
          //Find the DOM element with the citation
          var wells = this.$(".well"),
            viewRef = this;

          //Find the div.well with the citation. If we never find it, we don't insert the list of contents
          _.each(wells, function (well) {
            if (
              (!citationEl &&
                $(well).find("#viewMetadataCitationLink").length > 0) ||
              $(well).children(".row-fluid > .span10 > a")
            ) {
              //Save this element in the view
              citationEl = well;

              //Mark this in the DOM for CSS styling
              $(well).addClass("citation");

              //Save the text of the citation
              citation = $(well).text();
            }
          });

          //Remove the unnecessary classes that are used in older versions of Metacat (2.4.3 and older)
          var citationText = $(citationEl).find(".span10");
          $(citationText).removeClass("span10").addClass("span12");
        }

        //Set the document title to the citation
        MetacatUI.appModel.set("title", citation);

        citationEl.remove();
      },

      insertBreadcrumbs: function () {
        var breadcrumbs = $(document.createElement("ol"))
          .addClass("breadcrumb")
          .append(
            $(document.createElement("li"))
              .addClass("home")
              .append(
                $(document.createElement("a"))
                  .attr("href", MetacatUI.root || "/")
                  .addClass("home")
                  .text("Home"),
              ),
          )
          .append(
            $(document.createElement("li"))
              .addClass("search")
              .append(
                $(document.createElement("a"))
                  .attr(
                    "href",
                    MetacatUI.root +
                      "/data" +
                      (MetacatUI.appModel.get("page") > 0
                        ? "/page/" +
                          (parseInt(MetacatUI.appModel.get("page")) + 1)
                        : ""),
                  )
                  .addClass("search")
                  .text("Search"),
              ),
          )
          .append(
            $(document.createElement("li")).append(
              $(document.createElement("a"))
                .attr(
                  "href",
                  MetacatUI.root + "/view/" + encodeURIComponent(this.pid),
                )
                .addClass("inactive")
                .text("Metadata"),
            ),
          );

        if (MetacatUI.uiRouter.lastRoute() == "data") {
          $(breadcrumbs).prepend(
            $(document.createElement("a"))
              .attr(
                "href",
                MetacatUI.root +
                  "/data/page/" +
                  (MetacatUI.appModel.get("page") > 0
                    ? parseInt(MetacatUI.appModel.get("page")) + 1
                    : ""),
              )
              .attr("title", "Back")
              .addClass("back")
              .text(" Back to search")
              .prepend(
                $(document.createElement("i")).addClass("icon-angle-left"),
              ),
          );
          $(breadcrumbs).find("a.search").addClass("inactive");
        }

        this.$(this.breadcrumbContainer).html(breadcrumbs);
      },

      /*
       * When the metadata object doesn't exist, display a message to the user
       */
      showNotFound: function () {
        //If the model was found, exit this function
        if (!this.model.get("notFound")) {
          return;
        }

        try {
          //Check if a query string was in the URL and if so, try removing it in the identifier
          if (this.model.get("id").match(/\?\S+\=\S+/g) && !this.findTries) {
            let newID = this.model.get("id").replace(/\?\S+\=\S+/g, "");
            this.onClose();
            this.model.set("id", newID);
            this.pid = newID;
            this.findTries = 1;
            this.render();
            return;
          }
        } catch (e) {
          console.warn("Caught error while determining query string", e);
        }

        //Construct a message that shows this object doesn't exist
        var msg =
          "<h4>Nothing was found.</h4>" +
          "<p id='metadata-view-not-found-message'>The dataset identifier '" +
          Utilities.encodeHTML(this.model.get("id")) +
          "' " +
          "does not exist or it may have been removed. <a>Search for " +
          "datasets that mention " +
          Utilities.encodeHTML(this.model.get("id")) +
          "</a></p>";

        //Remove the loading message
        this.hideLoading();

        //Show the not found error message
        this.showError(msg);

        //Add the pid to the link href. Add via JS so it is Attribute-encoded to prevent XSS attacks
        this.$("#metadata-view-not-found-message a").attr(
          "href",
          MetacatUI.root +
            "/data/query=" +
            encodeURIComponent(this.model.get("id")),
        );
      },

      /*
       * When the metadata object is private, display a message to the user
       */
      showIsPrivate: function () {
        //If we haven't checked the logged-in status of the user yet, wait a bit
        //until we show a 401 msg, in case this content is their private content
        if (!MetacatUI.appUserModel.get("checked")) {
          this.listenToOnce(
            MetacatUI.appUserModel,
            "change:checked",
            this.showIsPrivate,
          );
          return;
        }

        //If the user is logged in, the message will display that this dataset is private.
        if (MetacatUI.appUserModel.get("loggedIn")) {
          var msg =
            '<span class="icon-stack private tooltip-this" data-toggle="tooltip"' +
            'data-placement="top" data-container="#metadata-controls-container"' +
            'title="" data-original-title="This is a private dataset.">' +
            '<i class="icon icon-circle icon-stack-base private"></i>' +
            '<i class="icon icon-lock icon-stack-top"></i>' +
            "</span> This is a private dataset.";
        }
        //If the user isn't logged in, display a log in link.
        else {
          var msg =
            '<span class="icon-stack private tooltip-this" data-toggle="tooltip"' +
            'data-placement="top" data-container="#metadata-controls-container"' +
            'title="" data-original-title="This is a private dataset.">' +
            '<i class="icon icon-circle icon-stack-base private"></i>' +
            '<i class="icon icon-lock icon-stack-top"></i>' +
            "</span> This is a private dataset. If you believe you have permission " +
            'to access this dataset, then <a href="' +
            MetacatUI.root +
            '/signin">sign in</a>.';
        }

        //Remove the loading message
        this.hideLoading();

        //Show the not found error message
        this.showError(msg);
      },

      getPackageDetails: function (packageIDs) {
        var completePackages = 0;

        //This isn't a package, but just a lonely metadata doc...
        if (!packageIDs || !packageIDs.length) {
          var thisPackage = new Package({ id: null, members: [this.model] });
          thisPackage.flagComplete();
          this.packageModels = [thisPackage];
          this.insertPackageDetails(thisPackage, {
            disablePackageDownloads: true,
          });
        } else {
          _.each(
            packageIDs,
            function (thisPackageID, i) {
              //Create a model representing the data package
              var thisPackage = new Package({ id: thisPackageID });

              //Listen for any parent packages
              this.listenToOnce(
                thisPackage,
                "change:parentPackageMetadata",
                this.insertParentLink,
              );

              //When the package info is fully retrieved
              this.listenToOnce(
                thisPackage,
                "complete",
                function (thisPackage) {
                  //When all packages are fully retrieved
                  completePackages++;
                  if (completePackages >= packageIDs.length) {
                    var latestPackages = _.filter(
                      this.packageModels,
                      function (m) {
                        return !_.contains(packageIDs, m.get("obsoletedBy"));
                      },
                    );

                    //Set those packages as the most recent package
                    this.packageModels = latestPackages;

                    this.insertPackageDetails(latestPackages);
                  }
                },
              );

              //Save the package in the view
              this.packageModels.push(thisPackage);

              //Make sure we get archived content, too
              thisPackage.set("getArchivedMembers", true);

              //Get the members
              thisPackage.getMembers({ getParentMetadata: true });
            },
            this,
          );
        }
      },

      alterMarkup: function () {
        //Find the taxonomic range and give it a class for styling - for older versions of Metacat only (v2.4.3 and older)
        if (!this.$(".taxonomicCoverage").length)
          this.$('h4:contains("Taxonomic Range")')
            .parent()
            .addClass("taxonomicCoverage");

        //Remove ecogrid links and replace them with workable links
        this.replaceEcoGridLinks();

        //Find the tab links for attribute names
        this.$(".attributeListTable tr a").on("shown", function (e) {
          //When the attribute link is clicked on, highlight the tab as active
          $(e.target)
            .parents(".attributeListTable")
            .find(".active")
            .removeClass("active");
          $(e.target).parents("tr").first().addClass("active");
        });

        //Mark the first row in each attribute list table as active since the first attribute is displayed at first
        this.$(".attributeListTable tr:first-child()").addClass("active");

        // Add explanation text to the alternate identifier
        this.renderAltIdentifierHelpText();
      },

      /**
       * Inserts an info icon next to the alternate identifier field, if it
       * exists. The icon will display a tooltip with the help text for the
       * field.
       * @returns {jQuery} The jQuery object for the icon element.
       * @since 2.26.0
       */
      renderAltIdentifierHelpText: function () {
        try {
          // Find the HTML element that contains the alternate identifier.
          const altIdentifierLabel = this.$(
            ".control-label:contains('Alternate Identifier')",
          );

          // It may not exist for all datasets.
          if (!altIdentifierLabel.length) return;

          const text = this.alternativeIdentifierHelpText;

          if (!text) return;

          // Create the tooltip
          const icon = $(document.createElement("i"))
            .addClass("tooltip-this icon icon-info-sign")
            .css("margin-left", "4px");

          // Activate the jQuery tooltip plugin
          icon.tooltip({
            title: text,
            placement: "top",
            container: "body",
          });

          // Add the icon to the label.
          altIdentifierLabel.append(icon);

          return icon;
        } catch (e) {
          console.log("Error adding help text to alternate identifier", e);
        }
      },

      /*
       * Inserts a table with all the data package member information and sends the call to display annotations
       */
      insertPackageDetails: function (packages, options) {
        if (typeof options === "undefined") {
          var options = {};
        }
        //Don't insert the package details twice
        var view = this;
        var tableEls = this.$(view.tableContainer).children().not(".loading");
        if (tableEls.length > 0) return;

        //wait for the metadata to load
        var metadataEls = this.$(view.metadataContainer).children();
        if (!metadataEls.length || metadataEls.first().is(".loading")) {
          this.once("metadataLoaded", function () {
            view.insertPackageDetails(this.packageModels, options);
          });
          return;
        }

        if (!packages) var packages = this.packageModels;

        //Get the entity names from this page/metadata
        this.getEntityNames(packages);

        _.each(
          packages,
          function (packageModel) {
            //If the package model is not complete, don't do anything
            if (!packageModel.complete) return;

            //Insert a package table for each package in viewRef dataset
            var nestedPckgs = packageModel.getNestedPackages(),
              nestedPckgsToDisplay = [];

            //If this metadata is not archived, filter out archived packages
            if (!this.model.get("archived")) {
              nestedPckgsToDisplay = _.reject(nestedPckgs, function (pkg) {
                return pkg.get("archived");
              });
            } else {
              //Display all packages is this metadata is archived
              nestedPckgsToDisplay = nestedPckgs;
            }

            if (nestedPckgsToDisplay.length > 0) {
              if (
                !(
                  !this.model.get("archived") &&
                  packageModel.get("archived") == true
                )
              ) {
                var title = packageModel.get("id")
                  ? '<span class="subtle">Package: ' +
                    packageModel.get("id") +
                    "</span>"
                  : "";
                options.title = "Files in this dataset " + title;
                options.nested = true;
                this.insertPackageTable(packageModel, options);
              }
            } else {
              //If this metadata is not archived, then don't display archived packages
              if (
                !(
                  !this.model.get("archived") &&
                  packageModel.get("archived") == true
                )
              ) {
                var title = packageModel.get("id")
                  ? '<span class="subtle">Package: ' +
                    packageModel.get("id") +
                    "</span>"
                  : "";
                options.title = "Files in this dataset " + title;
                this.insertPackageTable(packageModel, options);
              }
            }

            //Remove the extra download button returned from the XSLT since the package table will have all the download links
            $("#downloadPackage").remove();
          },
          this,
        );

        //If this metadata doc is not in a package, but is just a lonely metadata doc...
        if (!packages.length) {
          var packageModel = new Package({
            members: [this.model],
          });
          packageModel.complete = true;
          options.title = "Files in this dataset";
          options.disablePackageDownloads = true;
          this.insertPackageTable(packageModel, options);
        }

        //Insert the data details sections
        this.insertDataDetails();

        // Get data package, if there is one, before checking write permissions
        if (packages.length) {
          this.getDataPackage(packages[0].get("id"));
        } else {
          // Otherwise go ahead and check write permissions on metadata only
          this.checkWritePermissions();
        }

        try {
          // Get the most recent package to display the provenance graphs
          if (packages.length) {
            //Find the most recent Package model and fetch it
            let mostRecentPackage = _.find(
              packages,
              (p) => !p.get("obsoletedBy"),
            );

            //If all of the packages are obsoleted, then use the last package in the array,
            // which is most likely the most recent.
            /** @todo Use the DataONE version API to find the most recent package in the version chain */
            if (!mostRecentPackage) {
              mostRecentPackage = packages[packages.length - 1];
            }

            //Get the data package only if it is not the same as the previously fetched package
            if (mostRecentPackage.get("id") != packages[0].get("id"))
              this.getDataPackage(mostRecentPackage.get("id"));
          }
        } catch (e) {
          console.error(
            "Could not get the data package (prov will not be displayed, possibly other info as well).",
            e,
          );
        }

        //Initialize tooltips in the package table(s)
        this.$(".tooltip-this").tooltip();

        return this;
      },

      insertPackageTable: function (packageModel, options) {
        var view = this;
        if (this.dataPackage == null || !this.dataPackageSynced) {
          this.listenToOnce(this, "changed:dataPackageSynced", function () {
            view.insertPackageTable(packageModel, options);
          });
          return;
        }

        // Merge already fetched SolrResults into the dataPackage
        if (
          typeof packageModel !== "undefined" &&
          typeof packageModel.get("members") !== "undefined"
        ) {
          this.dataPackage.mergeModels(packageModel.get("members"));
        }

        if (options) {
          var title = options.title || "";
          var disablePackageDownloads =
            options.disablePackageDownloads || false;
          var nested =
            typeof options.nested === "undefined" ? false : options.nested;
        } else
          var title = "",
            nested = false,
            disablePackageDownloads = false;

        //** Draw the package table **//
        var tableView = new DataPackageView({
          edit: false,
          dataPackage: this.dataPackage,
          currentlyViewing: this.pid,
          dataEntities: this.entities,
          disablePackageDownloads: disablePackageDownloads,
          parentView: this,
          title: title,
          packageTitle: this.model.get("title"),
          nested: nested,
          metricsModel: this.metricsModel,
        });

        //Get the package table container
        var tablesContainer = this.$(this.tableContainer);

        //After the first table, start collapsing them
        var numTables = $(tablesContainer).find(
          "table.download-contents",
        ).length;
        if (numTables == 1) {
          var tableContainer = $(document.createElement("div")).attr(
            "id",
            "additional-tables-for-" + this.cid,
          );
          tableContainer.hide();
          $(tablesContainer).append(tableContainer);
        } else if (numTables > 1)
          var tableContainer = this.$("#additional-tables-for-" + this.cid);
        else var tableContainer = tablesContainer;

        //Insert the package table HTML
        $(tableContainer).empty();
        $(tableContainer).append(tableView.render().el);

        // Add Package Download
        // create an instance of DownloadButtonView to handle package downloads
        this.downloadButtonView = new DownloadButtonView({
          id: packageModel.get("id"),
          model: packageModel,
          view: "actionsView",
        });

        // render
        this.downloadButtonView.render();

        // add the downloadButtonView el to the span
        $(this.tableContainer)
          .find(".file-header .file-actions .downloadAction")
          .html(this.downloadButtonView.el);

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

        $(tableContainer).find(".tooltip-this").tooltip();

        this.subviews.push(tableView);

        //Trigger a custom event in this view that indicates the package table has been rendered
        this.trigger("dataPackageRendered");
      },

      insertParentLink: function (packageModel) {
        var parentPackageMetadata = packageModel.get("parentPackageMetadata"),
          view = this;

        _.each(parentPackageMetadata, function (m, i) {
          var title = m.get("title"),
            icon = $(document.createElement("i")).addClass(
              "icon icon-on-left icon-level-up",
            ),
            link = $(document.createElement("a"))
              .attr(
                "href",
                MetacatUI.root + "/view/" + encodeURIComponent(m.get("id")),
              )
              .addClass("parent-link")
              .text("Parent dataset: " + title)
              .prepend(icon);

          view.$(view.parentLinkContainer).append(link);
        });
      },

      insertSpatialCoverageMap: function (customCoordinates) {
        //Find the geographic region container. Older versions of Metacat (v2.4.3 and less) will not have it classified so look for the header text
        if (!this.$(".geographicCoverage").length) {
          //For EML
          var title = this.$('h4:contains("Geographic Region")');

          //For FGDC
          if (title.length == 0) {
            title = this.$('label:contains("Bounding Coordinates")');
          }

          var georegionEls = $(title).parent();
          var parseText = true;
          var directions = new Array("North", "South", "East", "West");
        } else {
          var georegionEls = this.$(".geographicCoverage");
          var directions = new Array("north", "south", "east", "west");
        }

        for (var i = 0; i < georegionEls.length; i++) {
          var georegion = georegionEls[i];

          if (typeof customCoordinates !== "undefined") {
            //Extract the coordinates
            var n = customCoordinates[0];
            var s = customCoordinates[1];
            var e = customCoordinates[2];
            var w = customCoordinates[3];
          } else {
            var coordinates = new Array();

            _.each(directions, function (direction) {
              //Parse text for older versions of Metacat (v2.4.3 and earlier)
              if (parseText) {
                var labelEl = $(georegion).find(
                  'label:contains("' + direction + '")',
                );
                if (labelEl.length) {
                  var coordinate = $(labelEl).next().html();
                  if (
                    typeof coordinate != "undefined" &&
                    coordinate.indexOf("&nbsp;") > -1
                  )
                    coordinate = coordinate.substring(
                      0,
                      coordinate.indexOf("&nbsp;"),
                    );
                }
              } else {
                var coordinate = $(georegion)
                  .find("." + direction + "BoundingCoordinate")
                  .attr("data-value");
              }

              //Save our coordinate value
              coordinates.push(coordinate);
            });

            //Extract the coordinates
            var n = coordinates[0];
            var s = coordinates[1];
            var e = coordinates[2];
            var w = coordinates[3];
          }

          //Create Google Map LatLng objects out of our coordinates
          var latLngSW = new gmaps.LatLng(s, w);
          var latLngNE = new gmaps.LatLng(n, e);
          var latLngNW = new gmaps.LatLng(n, w);
          var latLngSE = new gmaps.LatLng(s, e);

          //Get the centertroid location of this data item
          var bounds = new gmaps.LatLngBounds(latLngSW, latLngNE);
          var latLngCEN = bounds.getCenter();

          //If there isn't a center point found, don't draw the map.
          if (typeof latLngCEN == "undefined") {
            return;
          }

          //Get the map path color
          var pathColor = MetacatUI.appModel.get("datasetMapPathColor");
          if (pathColor) {
            pathColor = "color:" + pathColor + "|";
          } else {
            pathColor = "";
          }

          //Get the map path fill color
          var fillColor = MetacatUI.appModel.get("datasetMapFillColor");
          if (fillColor) {
            fillColor = "fillcolor:" + fillColor + "|";
          } else {
            fillColor = "";
          }

          //Create a google map image
          var mapHTML =
            "<img class='georegion-map' " +
            "src='https://maps.googleapis.com/maps/api/staticmap?" +
            "center=" +
            latLngCEN.lat() +
            "," +
            latLngCEN.lng() +
            "&size=800x350" +
            "&maptype=terrain" +
            "&markers=size:mid|color:0xDA4D3Aff|" +
            latLngCEN.lat() +
            "," +
            latLngCEN.lng() +
            "&path=" +
            fillColor +
            pathColor +
            "weight:3|" +
            latLngSW.lat() +
            "," +
            latLngSW.lng() +
            "|" +
            latLngNW.lat() +
            "," +
            latLngNW.lng() +
            "|" +
            latLngNE.lat() +
            "," +
            latLngNE.lng() +
            "|" +
            latLngSE.lat() +
            "," +
            latLngSE.lng() +
            "|" +
            latLngSW.lat() +
            "," +
            latLngSW.lng() +
            "&visible=" +
            latLngSW.lat() +
            "," +
            latLngSW.lng() +
            "|" +
            latLngNW.lat() +
            "," +
            latLngNW.lng() +
            "|" +
            latLngNE.lat() +
            "," +
            latLngNE.lng() +
            "|" +
            latLngSE.lat() +
            "," +
            latLngSE.lng() +
            "|" +
            latLngSW.lat() +
            "," +
            latLngSW.lng() +
            "&sensor=false" +
            "&key=" +
            MetacatUI.mapKey +
            "'/>";

          //Find the spot in the DOM to insert our map image
          if (parseText)
            var insertAfter = $(georegion)
              .find('label:contains("West")')
              .parent()
              .parent().length
              ? $(georegion).find('label:contains("West")').parent().parent()
              : georegion;
          //The last coordinate listed
          else var insertAfter = georegion;

          // Get the URL to the interactive Google Maps instance
          const url = this.getGoogleMapsUrl(latLngCEN, bounds);

          // Insert the map image
          $(insertAfter).append(
            this.mapTemplate({
              map: mapHTML,
              url: url,
            }),
          );

          $(".fancybox-media").fancybox({
            openEffect: "elastic",
            closeEffect: "elastic",
            helpers: {
              media: {},
            },
          });
        }

        return true;
      },

      /**
       * Returns a URL to a Google Maps instance that is centered on the given
       * coordinates and zoomed to the appropriate level to display the given
       * bounding box.
       * @param {LatLng} latLngCEN - The center point of the map.
       * @param {LatLngBounds} bounds - The bounding box to display.
       * @returns {string} The URL to the Google Maps instance.
       * @since 2.27.0
       */
      getGoogleMapsUrl: function (latLngCEN, bounds) {
        // Use the window width and height as a proxy for the map dimensions
        const mapDim = {
          height: $(window).height(),
          width: $(window).width(),
        };
        const z = this.getBoundsZoomLevel(bounds, mapDim);
        const mapLat = latLngCEN.lat();
        const mapLng = latLngCEN.lng();

        return `https://maps.google.com/?ll=${mapLat},${mapLng}&z=${z}`;
      },

      /**
       * Returns the zoom level that will display the given bounding box at
       * the given dimensions.
       * @param {LatLngBounds} bounds - The bounding box to display.
       * @param {Object} mapDim - The dimensions of the map.
       * @param {number} mapDim.height - The height of the map.
       * @param {number} mapDim.width - The width of the map.
       * @returns {number} The zoom level.
       * @since 2.27.0
       */
      getBoundsZoomLevel: function (bounds, mapDim) {
        var WORLD_DIM = { height: 256, width: 256 };
        var ZOOM_MAX = 15;
        // 21 is actual max, but any closer and the map is too zoomed in to be
        // useful

        function latRad(lat) {
          var sin = Math.sin((lat * Math.PI) / 180);
          var radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
          return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
        }

        function zoom(mapPx, worldPx, fraction) {
          return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2);
        }

        var ne = bounds.getNorthEast();
        var sw = bounds.getSouthWest();

        var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI;

        var lngDiff = ne.lng() - sw.lng();
        var lngFraction = (lngDiff < 0 ? lngDiff + 360 : lngDiff) / 360;

        var latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
        var lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);

        return Math.min(latZoom, lngZoom, ZOOM_MAX);
      },

      insertCitation: function () {
        if (!this.model) return false;
        //Create a citation header element from the model attributes
        var header = new CitationHeaderView({ model: this.model });
        this.$(this.citationContainer).html(header.render().el);
      },

      insertDataSource: function () {
        if (
          !this.model ||
          !MetacatUI.nodeModel ||
          !MetacatUI.nodeModel.get("members").length ||
          !this.$(this.dataSourceContainer).length
        )
          return;

        var dataSource = MetacatUI.nodeModel.getMember(this.model),
          replicaMNs = MetacatUI.nodeModel.getMembers(
            this.model.get("replicaMN"),
          );

        //Filter out the data source from the replica nodes
        if (Array.isArray(replicaMNs) && replicaMNs.length) {
          replicaMNs = _.without(replicaMNs, dataSource);
        }

        if (dataSource && dataSource.logo) {
          this.$("img.data-source").remove();

          //Construct a URL to the profile of this repository
          var profileURL =
            dataSource.identifier == MetacatUI.appModel.get("nodeId")
              ? MetacatUI.root + "/profile"
              : MetacatUI.appModel.get("dataoneSearchUrl") +
                "/portals/" +
                dataSource.shortIdentifier;

          //Insert the data source template
          this.$(this.dataSourceContainer)
            .html(
              this.dataSourceTemplate({
                node: dataSource,
                profileURL: profileURL,
              }),
            )
            .addClass("has-data-source");

          this.$(this.citationContainer).addClass("has-data-source");
          this.$(".tooltip-this").tooltip();

          $(".popover-this.data-source.logo")
            .popover({
              trigger: "manual",
              html: true,
              title: "From the " + dataSource.name + " repository",
              content: function () {
                var content = "<p>" + dataSource.description + "</p>";

                if (replicaMNs.length) {
                  content +=
                    "<h5>Exact copies hosted by " +
                    replicaMNs.length +
                    ' repositories: </h5><ul class="unstyled">';

                  _.each(replicaMNs, function (node) {
                    content +=
                      '<li><a href="' +
                      MetacatUI.appModel.get("dataoneSearchUrl") +
                      "/portals/" +
                      node.shortIdentifier +
                      '" class="pointer">' +
                      node.name +
                      "</a></li>";
                  });

                  content += "</ul>";
                }

                return content;
              },
              animation: false,
            })
            .on("mouseenter", function () {
              var _this = this;
              $(this).popover("show");
              $(".popover").on("mouseleave", function () {
                $(_this).popover("hide");
              });
            })
            .on("mouseleave", function () {
              var _this = this;
              setTimeout(function () {
                if (!$(".popover:hover").length) {
                  $(_this).popover("hide");
                }
              }, 300);
            });
        }
      },

      /**
       * Check whether the user has write permissions on the resource map and the EML.
       * Once the permission checks have finished, continue with the functions that
       * depend on them.
       */
      checkWritePermissions: function () {
        var view = this,
          authorization = [],
          resourceMap = this.dataPackage ? this.dataPackage.packageModel : null,
          modelsToCheck = [this.model, resourceMap];

        modelsToCheck.forEach(function (model, index) {
          // If there is no resource map or no EML,
          // then the user does not need permission to edit it.
          if (!model || model.get("notFound") == true) {
            authorization[index] = true;
            // If we already checked, and the user is authorized,
            // record that information in the authorzation array.
          } else if (model.get("isAuthorized_write") === true) {
            authorization[index] = true;
            // If we already checked, and the user is not authorized,
            // record that information in the authorzation array.
          } else if (model.get("isAuthorized_write") === false) {
            authorization[index] = false;
            // If we haven't checked for authorization yet, do that now.
            // Return to this function once we've finished checking.
          } else {
            view.stopListening(model, "change:isAuthorized_write");
            view.listenToOnce(model, "change:isAuthorized_write", function () {
              view.checkWritePermissions();
            });
            view.stopListening(model, "change:notFound");
            view.listenToOnce(model, "change:notFound", function () {
              view.checkWritePermissions();
            });
            model.checkAuthority("write");
            return;
          }
        });

        // Check that all the models were tested for authorization

        // Every value in the auth array must be true for the user to have full permissions
        var allTrue = _.every(authorization, function (test) {
            return test;
          }),
          // When we have completed checking each of the models that we need to check for
          // permissions, every value in the authorization array should be "true" or "false",
          // and the array should have the same length as the modelsToCheck array.
          allBoolean = _.every(authorization, function (test) {
            return typeof test === "boolean";
          }),
          allChecked =
            allBoolean && authorization.length === modelsToCheck.length;

        // Check for and render prov diagrams now that we know whether or not the user has editor permissions
        // (There is a different version of the chart for users who can edit the resource map and users who cannot)
        if (allChecked) {
          this.checkForProv();
        } else {
          return;
        }
        // Only render the editor controls if we have completed the checks AND the user has full editor permissions
        if (allTrue) {
          this.insertEditorControls();
        }
      },

      /*
       * Inserts control elements onto the page for the user to interact with the dataset - edit, publish, etc.
       * Editor permissions should already have been checked before running this function.
       */
      insertEditorControls: function () {
        var view = this,
          resourceMap = this.dataPackage ? this.dataPackage.packageModel : null,
          modelsToCheck = [this.model, resourceMap],
          authorized = _.every(modelsToCheck, function (model) {
            // If there is no EML or no resource map, the user doesn't need permission to edit it.
            return !model || model.get("notFound") == true
              ? true
              : model.get("isAuthorized_write") === true;
          });

        // Only run this function when the user has full editor permissions
        // (i.e. write permission on the EML, and write permission on the resource map if there is one.)
        if (!authorized) {
          return;
        }

        if (
          (this.model.get("obsoletedBy") &&
            this.model.get("obsoletedBy").length > 0) ||
          this.model.get("archived")
        ) {
          return false;
        }

        // Save the element that will contain the owner control buttons
        var container = this.$(this.editorControlsContainer);
        // Do not insert the editor controls twice
        container.empty();

        // The PID for the EML model
        var pid = this.model.get("id") || this.pid;

        //Insert an Edit button if the Edit button is enabled
        if (MetacatUI.appModel.get("displayDatasetEditButton")) {
          //Check that this is an editable metadata format
          if (
            _.contains(
              MetacatUI.appModel.get("editableFormats"),
              this.model.get("formatId"),
            )
          ) {
            //Insert the Edit Metadata template
            container.append(
              this.editMetadataTemplate({
                identifier: pid,
                supported: true,
              }),
            );
          }
          //If this format is not editable, insert an unspported Edit Metadata template
          else {
            container.append(
              this.editMetadataTemplate({
                supported: false,
              }),
            );
          }
        }

        try {
          //Determine if this metadata can be published.
          // The Publish feature has to be enabled in the app.
          // The model cannot already have a DOI
          var canBePublished =
            MetacatUI.appModel.get("enablePublishDOI") && !view.model.isDOI();

          //If publishing is enabled, check if only certain users and groups can publish metadata
          if (canBePublished) {
            //Get the list of authorized publishers from the AppModel
            var authorizedPublishers = MetacatUI.appModel.get(
              "enablePublishDOIForSubjects",
            );
            //If the logged-in user is one of the subjects in the list or is in a group that is
            // in the list, then this metadata can be published. Otherwise, it cannot.
            if (
              Array.isArray(authorizedPublishers) &&
              authorizedPublishers.length
            ) {
              if (
                MetacatUI.appUserModel.hasIdentityOverlap(authorizedPublishers)
              ) {
                canBePublished = true;
              } else {
                canBePublished = false;
              }
            }
          }

          //If this metadata can be published, then insert the Publish button template
          if (canBePublished) {
            //Insert a Publish button template
            container.append(
              view.doiTemplate({
                isAuthorized: true,
                identifier: pid,
              }),
            );
          }
        } catch (e) {
          console.error("Cannot display the publish button: ", e);
        }
      },

      /*
       * Injects Clipboard objects onto DOM elements returned from the Metacat
       * View Service. This code depends on the implementation of the Metacat
       * View Service in that it depends on elements with the class "copy" being
       * contained in the HTML returned from the View Service.
       *
       * To add more copiable buttons (or other elements) to a View Service XSLT,
       * you should be able to just add something like:
       *
       *   <button class="btn copy" data-clipboard-text="your-text-to-copy">
       *      Copy
       *   </button>
       *
       * to your XSLT and this should pick it up automatically.
       */
      insertCopiables: function () {
        var copiables = $("#Metadata .copy");

        _.each(copiables, function (copiable) {
          var clipboard = new Clipboard(copiable);

          clipboard.on("success", function (e) {
            var el = $(e.trigger);

            $(el).html(
              $(document.createElement("span")).addClass(
                "icon icon-ok success",
              ),
            );

            // Use setTimeout instead of jQuery's built-in Events system because
            // it didn't look flexible enough to allow me update innerHTML in
            // a chain
            setTimeout(function () {
              $(el).html("Copy");
            }, 500);
          });
        });
      },

      /*
       * Inserts elements users can use to interact with this dataset:
       * - A "Copy Citation" button to copy the citation text
       */
      insertControls: function () {
        // Convert the support mdq formatId list to a version
        // that JS regex likes (with special characters double
        RegExp.escape = function (s) {
          return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\\\$&");
        };
        var mdqFormatIds = MetacatUI.appModel.get("mdqFormatIds");

        // Check of the current formatId is supported by the current
        // metadata quality suite. If not, the 'Assessment Report' button
        // will not be displacyed in the metadata controls panel.
        var thisFormatId = this.model.get("formatId");
        var mdqFormatSupported = false;
        var formatFound = false;
        if (mdqFormatIds !== null) {
          for (var ifmt = 0; ifmt < mdqFormatIds.length; ++ifmt) {
            var currentFormatId = RegExp.escape(mdqFormatIds[ifmt]);
            var re = new RegExp(currentFormatId);
            formatFound = re.test(thisFormatId);
            if (formatFound) {
              break;
            }
          }
        }

        //Get template
        var controlsContainer = this.controlsTemplate({
          citationTarget: this.citationContainer,
          url: window.location,
          displayQualtyReport:
            MetacatUI.appModel.get("mdqBaseUrl") &&
            formatFound &&
            MetacatUI.appModel.get("displayDatasetQualityMetric"),
          showWholetale: MetacatUI.appModel.get("showWholeTaleFeatures"),
          model: this.model.toJSON(),
        });

        $(this.controlsContainer).html(controlsContainer);

        //Insert the info icons
        var metricsWell = this.$(".metrics-container");
        metricsWell.append(
          this.infoIconsTemplate({
            model: this.model.toJSON(),
          }),
        );

        if (MetacatUI.appModel.get("showWholeTaleFeatures")) {
          this.createWholeTaleButton();
        }

        // Show the citation modal with the ability to copy the citation text
        // when the "Copy Citation" button is clicked
        const citeButton = this.el.querySelector("#cite-this-dataset-btn");
        if (citeButton) {
          citeButton.removeEventListener("click", this.citationModal);
          citeButton.addEventListener(
            "click",
            () => {
              this.citationModal = new CitationModalView({
                model: this.model,
                createLink: true,
              });
              this.subviews.push(this.citationModal);
              this.citationModal.render();
            },
            false,
          );
        }
      },

      /**
       *Creates a button which the user can click to launch the package in Whole Tale
       */
      createWholeTaleButton: function () {
        let self = this;
        MetacatUI.appModel
          .get("taleEnvironments")
          .forEach(function (environment) {
            var queryParams =
              "?uri=" +
              window.location.href +
              "&title=" +
              encodeURIComponent(self.model.get("title")) +
              "&environment=" +
              environment +
              "&api=" +
              MetacatUI.appModel.get("d1CNBaseUrl") +
              MetacatUI.appModel.get("d1CNService");
            var composeUrl =
              MetacatUI.appModel.get("dashboardUrl") + queryParams;
            var anchor = $("<a>");
            anchor
              .attr("href", composeUrl)
              .append($("<span>").attr("class", "tab").append(environment));
            anchor.attr("target", "_blank");
            $(".analyze.dropdown-menu").append($("<li>").append(anchor));
          });
      },

      // Inserting the Metric Stats
      insertMetricsControls: function () {
        //Exit if metrics shouldn't be shown for this dataset
        if (this.model.hideMetrics()) {
          return;
        }

        var pid_list = [];
        pid_list.push(this.pid);
        var metricsModel = new MetricsModel({
          pid_list: pid_list,
          type: "dataset",
        });
        metricsModel.fetch();
        this.metricsModel = metricsModel;

        // Retreive the model from the server for the given PID
        // TODO: Create a Metric Request Object

        if (MetacatUI.appModel.get("displayDatasetMetrics")) {
          var buttonToolbar = this.$(".metrics-container");

          if (MetacatUI.appModel.get("displayDatasetDownloadMetric")) {
            var dwnldsMetricView = new MetricView({
              metricName: "Downloads",
              model: metricsModel,
              pid: this.pid,
            });
            buttonToolbar.append(dwnldsMetricView.render().el);
            this.subviews.push(dwnldsMetricView);
          }

          if (MetacatUI.appModel.get("displayDatasetCitationMetric")) {
            var citationsMetricView = new MetricView({
              metricName: "Citations",
              model: metricsModel,
              pid: this.pid,
            });
            buttonToolbar.append(citationsMetricView.render().el);
            this.subviews.push(citationsMetricView);

            try {
              //Check if the registerCitation=true query string is set
              if (window.location.search) {
                if (
                  window.location.search.indexOf("registerCitation=true") > -1
                ) {
                  //Open the modal for the citations
                  citationsMetricView.showMetricModal();

                  //Show the register citation form
                  if (citationsMetricView.modalView) {
                    citationsMetricView.modalView.on(
                      "renderComplete",
                      citationsMetricView.modalView.showCitationForm,
                    );
                  }
                }
              }
            } catch (e) {
              console.warn("Not able to show the register citation form ", e);
            }
          }

          if (MetacatUI.appModel.get("displayDatasetViewMetric")) {
            var viewsMetricView = new MetricView({
              metricName: "Views",
              model: metricsModel,
              pid: this.pid,
            });
            buttonToolbar.append(viewsMetricView.render().el);
            this.subviews.push(viewsMetricView);
          }
        }
      },

      /**
       * Check if the DataPackage provenance parsing has completed. If it has,
       * draw provenance charts. If it hasn't start the parseProv function.
       * The view must have the DataPackage collection set as view.dataPackage
       * for this function to run.
       */
      checkForProv: function () {
        if (!this.dataPackage) {
          return;
        }
        // Render the provenance trace using the redrawProvCharts function instead of the drawProvCharts function
        // just in case the prov charts have already been inserted. Redraw will make sure they are removed
        // before being re-inserted.
        var model = this.model;
        if (this.dataPackage.provenanceFlag == "complete") {
          this.redrawProvCharts(this.dataPackage);
        } else {
          this.listenToOnce(this.dataPackage, "queryComplete", function () {
            this.redrawProvCharts(this.dataPackage);
          });
          // parseProv triggers "queryComplete"
          this.dataPackage.parseProv();
        }
      },

      /*
       * Renders ProvChartViews on the page to display provenance on a package level and on an individual object level.
       * This function looks at four sources for the provenance - the package sources, the package derivations, member sources, and member derivations
       */
      drawProvCharts: function (dataPackage) {
        // Set a listener to re-draw the prov charts when needed
        this.stopListening(this.dataPackage, "redrawProvCharts");
        this.listenToOnce(
          this.dataPackage,
          "redrawProvCharts",
          this.redrawProvCharts,
        );

        // Provenance has to be retrieved from the Package Model (getProvTrace()) before the charts can be drawn
        if (dataPackage.provenanceFlag != "complete") return false;

        // If the user is authorized to edit the provenance for this package
        // then turn on editing, so that edit icons are displayed.
        var editModeOn =
          this.dataPackage.packageModel.get("isAuthorized_write");

        //If this content is archived, then turn edit mode off
        if (this.model.get("archived")) {
          editModeOn = false;
        }

        //If none of the models in this package have the formatId attributes,
        // we should fetch the DataPackage since it likely has only had a shallow fetch so far
        var formats = _.compact(dataPackage.pluck("formatId"));

        //If the number of formatIds is less than the number of models in this collection,
        // then we need to get them.
        if (formats.length < dataPackage.length) {
          var modelsToMerge = [];

          //Get the PackageModel associated with this view
          if (this.packageModels.length) {
            //Get the PackageModel for this DataPackage
            var packageModel = _.find(
              this.packageModels,
              function (packageModel) {
                return packageModel.get("id") == dataPackage.id;
              },
            );

            //Merge the SolrResult models into the DataONEObject models
            if (packageModel && packageModel.get("members").length) {
              modelsToMerge = packageModel.get("members");
            }
          }

          //If there is at least one model to merge into this data package, do so
          if (modelsToMerge.length) {
            dataPackage.mergeModels(modelsToMerge);
          }
          //If there are no models to merge in, get them from the index
          else {
            //Listen to the DataPackage fetch to complete and re-execute this function
            this.listenToOnce(dataPackage, "complete", function () {
              this.drawProvCharts(dataPackage);
            });

            //Create a query that searches for all the members of this DataPackage in Solr
            dataPackage.solrResults.currentquery =
              dataPackage.filterModel.getQuery() +
              "%20AND%20-formatType:METADATA";
            dataPackage.solrResults.fields = "id,seriesId,formatId,fileName";
            dataPackage.solrResults.rows = dataPackage.length;
            dataPackage.solrResults.sort = null;
            dataPackage.solrResults.start = 0;
            dataPackage.solrResults.facet = [];
            dataPackage.solrResults.stats = null;

            //Fetch the data package with the "fromIndex" option
            dataPackage.fetch({ fromIndex: true });

            //Exit this function since it will be executed again when the fetch is complete
            return;
          }
        }

        var view = this;
        //Draw two flow charts to represent the sources and derivations at a package level
        var packageSources = dataPackage.sourcePackages;
        var packageDerivations = dataPackage.derivationPackages;

        if (Object.keys(packageSources).length) {
          var sourceProvChart = new ProvChart({
            sources: packageSources,
            context: dataPackage,
            contextEl: this.$(this.articleContainer),
            dataPackage: dataPackage,
            parentView: view,
          });
          this.subviews.push(sourceProvChart);
          this.$(this.articleContainer).before(sourceProvChart.render().el);
        }
        if (Object.keys(packageDerivations).length) {
          var derivationProvChart = new ProvChart({
            derivations: packageDerivations,
            context: dataPackage,
            contextEl: this.$(this.articleContainer),
            dataPackage: dataPackage,
            parentView: view,
          });
          this.subviews.push(derivationProvChart);
          this.$(this.articleContainer).after(derivationProvChart.render().el);
        }

        if (
          dataPackage.sources.length ||
          dataPackage.derivations.length ||
          editModeOn
        ) {
          //Draw the provenance charts for each member of this package at an object level
          _.each(dataPackage.toArray(), function (member, i) {
            // Don't draw prov charts for metadata objects.
            if (
              member.get("type").toLowerCase() == "metadata" ||
              member.get("formatType").toLowerCase() == "metadata"
            ) {
              return;
            }
            var entityDetailsSection = view.findEntityDetailsContainer(member);

            if (!entityDetailsSection) {
              return;
            }

            //Retrieve the sources and derivations for this member
            var memberSources = member.get("provSources") || new Array(),
              memberDerivations = member.get("provDerivations") || new Array();

            //Make the source chart for this member.
            // If edit is on, then either a 'blank' sources ProvChart will be displayed if there
            // are no sources for this member, or edit icons will be displayed with prov icons.
            if (memberSources.length || editModeOn) {
              var memberSourcesProvChart = new ProvChart({
                sources: memberSources,
                context: member,
                contextEl: entityDetailsSection,
                dataPackage: dataPackage,
                parentView: view,
                editModeOn: editModeOn,
                editorType: "sources",
              });
              view.subviews.push(memberSourcesProvChart);
              $(entityDetailsSection).before(
                memberSourcesProvChart.render().el,
              );
              view.$(view.articleContainer).addClass("gutters");
            }

            //Make the derivation chart for this member
            // If edit is on, then either a 'blank' derivations ProvChart will be displayed if there,
            // are no derivations for this member or edit icons will be displayed with prov icons.
            if (memberDerivations.length || editModeOn) {
              var memberDerivationsProvChart = new ProvChart({
                derivations: memberDerivations,
                context: member,
                contextEl: entityDetailsSection,
                dataPackage: dataPackage,
                parentView: view,
                editModeOn: editModeOn,
                editorType: "derivations",
              });
              view.subviews.push(memberDerivationsProvChart);
              $(entityDetailsSection).after(
                memberDerivationsProvChart.render().el,
              );
              view.$(view.articleContainer).addClass("gutters");
            }
          });
        }

        //Make all of the prov chart nodes look different based on id
        if (this.$(".prov-chart").length > 10000) {
          var allNodes = this.$(".prov-chart .node"),
            ids = [],
            view = this,
            i = 1;

          $(allNodes).each(function () {
            ids.push($(this).attr("data-id"));
          });
          ids = _.uniq(ids);

          _.each(ids, function (id) {
            var matchingNodes = view
              .$(".prov-chart .node[data-id='" + id + "']")
              .not(".editorNode");
            //var matchingEntityDetails = view.findEntityDetailsContainer(id);

            //Don't use the unique class on images since they will look a lot different anyway by their image
            if (!$(matchingNodes).first().hasClass("image")) {
              var className = "uniqueNode" + i;

              //Add the unique class and up the iterator
              if (matchingNodes.prop("tagName") != "polygon")
                $(matchingNodes).addClass(className);
              else
                $(matchingNodes).attr(
                  "class",
                  $(matchingNodes).attr("class") + " " + className,
                );

              /*  if(matchingEntityDetails)
                    $(matchingEntityDetails).addClass(className);*/

              //Save this id->class mapping in this view
              view.classMap.push({
                id: id,
                className: className,
              });
              i++;
            }
          });
        }
      },

      /* Step through all prov charts and re-render each one that has been
           marked for re-rendering.
        */
      redrawProvCharts: function () {
        var view = this;

        // Check if prov edits are active and turn on the prov save bar if so.
        // Alternatively, turn off save bar if there are no prov edits, which
        // could occur if a user undoes a previous which could result in
        // an empty edit list.
        if (this.dataPackage.provEditsPending()) {
          this.showEditorControls();
        } else {
          this.hideEditorControls();

          // Reset the edited flag for each package member
          _.each(this.dataPackage.toArray(), function (item) {
            item.selectedInEditor == false;
          });
        }
        _.each(this.subviews, function (thisView, i) {
          // Check if this is a ProvChartView
          if (
            thisView.className &&
            thisView.className.indexOf("prov-chart") !== -1
          ) {
            // Check if this ProvChartView is marked for re-rendering
            // Erase the current ProvChartView
            thisView.onClose();
          }
        });

        // Remove prov charts from the array of subviews.
        this.subviews = _.filter(this.subviews, function (item) {
          return item.className && item.className.indexOf("prov-chart") == -1;
        });

        view.drawProvCharts(this.dataPackage);
      },

      /*
       * When the data package collection saves successfully, tell the user
       */
      saveSuccess: function (savedObject) {
        //We only want to perform these actions after the package saves
        if (savedObject.type != "DataPackage") return;

        //Change the URL to the new id
        MetacatUI.uiRouter.navigate(
          "view/" + this.dataPackage.packageModel.get("id"),
          { trigger: false, replace: true },
        );

        var message = $(document.createElement("div")).append(
          $(document.createElement("span")).text(
            "Your changes have been saved. ",
          ),
        );

        MetacatUI.appView.showAlert(message, "alert-success", "body", 4000, {
          remove: false,
        });

        // Reset the state to clean
        this.dataPackage.packageModel.set("changed", false);

        // If provenance relationships were updated, then reset the edit list now.
        if (this.dataPackage.provEdits.length) this.dataPackage.provEdits = [];

        this.saveProvPending = false;
        this.hideSaving();
        this.stopListening(this.dataPackage, "errorSaving", this.saveError);

        // Turn off "save" footer
        this.hideEditorControls();

        // Update the metadata table header with the new resource map id.
        // First find the DataPackageView for the top level package, and
        // then re-render it with the update resmap id.
        var view = this;
        var metadataId = this.packageModels[0].getMetadata().get("id");
        _.each(this.subviews, function (thisView, i) {
          // Check if this is a ProvChartView
          if (thisView.type && thisView.type.indexOf("DataPackage") !== -1) {
            if (thisView.currentlyViewing == metadataId) {
              var packageId = view.dataPackage.packageModel.get("id");
              var title = packageId
                ? '<span class="subtle">Package: ' + packageId + "</span>"
                : "";
              thisView.title = "Files in this dataset " + title;
              thisView.render();
            }
          }
        });
      },

      /*
       * When the data package collection fails to save, tell the user
       */
      saveError: function (errorMsg) {
        var errorId = "error" + Math.round(Math.random() * 100),
          message = $(document.createElement("div")).append(
            "<p>Your changes could not be saved.</p>",
          );

        message.append(
          $(document.createElement("a"))
            .text("See details")
            .attr("data-toggle", "collapse")
            .attr("data-target", "#" + errorId)
            .addClass("pointer"),
          $(document.createElement("div"))
            .addClass("collapse")
            .attr("id", errorId)
            .append($(document.createElement("pre")).text(errorMsg)),
        );

        MetacatUI.appView.showAlert(message, "alert-error", "body", null, {
          emailBody: "Error message: Data Package save error: " + errorMsg,
          remove: true,
        });

        this.saveProvPending = false;
        this.hideSaving();
        this.stopListening(this.dataPackage, "successSaving", this.saveSuccess);

        // Turn off "save" footer
        this.hideEditorControls();
      },

      /* If provenance relationships have been modified by the provenance editor (in ProvChartView), then
        update the ORE Resource Map and save it to the server.
        */
      saveProv: function () {
        // Only call this function once per save operation.
        if (this.saveProvPending) return;

        var view = this;
        if (this.dataPackage.provEditsPending()) {
          this.saveProvPending = true;
          // If the Data Package failed saving, display an error message
          this.listenToOnce(this.dataPackage, "errorSaving", this.saveError);
          // Listen for when the package has been successfully saved
          this.listenToOnce(
            this.dataPackage,
            "successSaving",
            this.saveSuccess,
          );
          this.showSaving();
          this.dataPackage.saveProv();
        } else {
          //TODO: should a dialog be displayed saying that no prov edits were made?
        }
      },

      showSaving: function () {
        //Change the style of the save button
        this.$("#save-metadata-prov")
          .html('<i class="icon icon-spinner icon-spin"></i> Saving...')
          .addClass("btn-disabled");

        this.$("input, textarea, select, button").prop("disabled", true);
      },

      hideSaving: function () {
        this.$("input, textarea, select, button").prop("disabled", false);

        //When prov is saved, revert the Save button back to normal
        this.$("#save-metadata-prov").html("Save").removeClass("btn-disabled");
      },

      showEditorControls: function () {
        this.$("#editor-footer").slideDown();
      },

      hideEditorControls: function () {
        this.$("#editor-footer").slideUp();
      },

      getEntityNames: function (packageModels) {
        var viewRef = this;

        _.each(packageModels, function (packageModel) {
          //Don't get entity names for larger packages - users must put the names in the system metadata
          if (packageModel.get("members").length > 100) return;

          //If this package has a different metadata doc than the one we are currently viewing
          var metadataModel = packageModel.getMetadata();
          if (!metadataModel) return;

          if (metadataModel.get("id") != viewRef.pid) {
            var requestSettings = {
              url:
                MetacatUI.appModel.get("viewServiceUrl") +
                encodeURIComponent(metadataModel.get("id")),
              success: function (parsedMetadata, response, xhr) {
                _.each(packageModel.get("members"), function (solrResult, i) {
                  var entityName = "";

                  if (solrResult.get("formatType") == "METADATA")
                    entityName = solrResult.get("title");

                  var container = viewRef.findEntityDetailsContainer(
                    solrResult,
                    parsedMetadata,
                  );
                  if (container) entityName = viewRef.getEntityName(container);

                  //Set the entity name
                  if (entityName) {
                    solrResult.set("fileName", entityName);
                    //Update the UI with the new name
                    viewRef
                      .$(
                        ".entity-name-placeholder[data-id='" +
                          solrResult.get("id") +
                          "']",
                      )
                      .text(entityName);
                  }
                });
              },
            };

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

            return;
          }

          _.each(packageModel.get("members"), function (solrResult, i) {
            var entityName = "";

            if (solrResult.get("fileName"))
              entityName = solrResult.get("fileName");
            else if (solrResult.get("formatType") == "METADATA")
              entityName = solrResult.get("title");
            else if (solrResult.get("formatType") == "RESOURCE") return;
            else {
              var container = viewRef.findEntityDetailsContainer(solrResult);

              if (container && container.length > 0)
                entityName = viewRef.getEntityName(container);
              else entityName = null;
            }

            //Set the entityName, even if it's null
            solrResult.set("fileName", entityName);
          });
        });
      },

      getEntityName: function (containerEl) {
        if (!containerEl) return false;

        var entityName = $(containerEl)
          .find(".entityName")
          .attr("data-entity-name");
        if (typeof entityName === "undefined" || !entityName) {
          entityName = $(containerEl)
            .find(".control-label:contains('Entity Name') + .controls-well")
            .text();
          if (typeof entityName === "undefined" || !entityName)
            entityName = null;
        }

        return entityName;
      },

      //Checks if the metadata has entity details sections
      hasEntityDetails: function () {
        return this.$(".entitydetails").length > 0;
      },

      /**
       * Finds the element in the rendered metadata that describes the given data entity.
       *
       * @param {(DataONEObject|SolrResult|string)} model - Either a model that represents the data object or the identifier of the data object
       * @param {Element} [el] - The DOM element to exclusivly search inside.
       * @return {Element} - The DOM element that describbbes the given data entity.
       */
      findEntityDetailsContainer: function (model, el) {
        if (!el) var el = this.el;

        //Get the id and file name for this data object
        var id = "",
          fileName = "";

        //If a model is given, get the id and file name from the object
        if (
          model &&
          (DataONEObject.prototype.isPrototypeOf(model) ||
            SolrResult.prototype.isPrototypeOf(model))
        ) {
          id = model.get("id");
          fileName = model.get("fileName");
        }
        //If a string is given instead, it must be the id of the data object
        else if (typeof model == "string") {
          id = model;
        }
        //Otherwise, there isn't enough info to find the element, so exit
        else {
          return;
        }

        //If we already found it earlier, return it now
        var container = this.$(
          ".entitydetails[data-id='" +
            id +
            "'], " +
            ".entitydetails[data-id='" +
            DataONEObject.prototype.getXMLSafeID(id) +
            "']",
        );
        if (container.length) return container;

        //Are we looking for the main object that this MetadataView is displaying?
        if (id == this.pid) {
          if (this.$("#Metadata").length > 0) return this.$("#Metadata");
          else return this.el;
        }

        //Metacat 2.4.2 and up will have the Online Distribution Link marked
        var link = this.$(".entitydetails a[data-pid='" + id + "']");

        //Otherwise, try looking for an anchor with the id matching this object's id
        if (!link.length)
          link = $(el).find("a#" + id.replace(/[^A-Za-z0-9]/g, "\\$&"));

        //Get metadata index view
        var metadataFromIndex = _.findWhere(this.subviews, {
          type: "MetadataIndex",
        });
        if (typeof metadataFromIndex === "undefined") metadataFromIndex = null;

        //Otherwise, find the Online Distribution Link the hard way
        if (link.length < 1 && !metadataFromIndex)
          link = $(el).find(
            ".control-label:contains('Online Distribution Info') + .controls-well > a[href*='" +
              id.replace(/[^A-Za-z0-9]/g, "\\$&") +
              "']",
          );

        if (link.length > 0) {
          //Get the container element
          container = $(link).parents(".entitydetails");

          if (container.length < 1) {
            //backup - find the parent of this link that is a direct child of the form element
            var firstLevelContainer = _.intersection(
              $(link).parents("form").children(),
              $(link).parents(),
            );
            //Find the controls-well inside of that first level container, which is the well that contains info about this data object
            if (firstLevelContainer.length > 0)
              container = $(firstLevelContainer).children(".controls-well");

            if (container.length < 1 && firstLevelContainer.length > 0)
              container = firstLevelContainer;

            $(container).addClass("entitydetails");
          }

          //Add the id so we can easily find it later
          container.attr("data-id", id);

          return container;
        }

        //----Find by file name rather than id-----
        if (!fileName) {
          //Get the name of the object first
          for (var i = 0; i < this.packageModels.length; i++) {
            var model = _.findWhere(this.packageModels[i].get("members"), {
              id: id,
            });
            if (model) {
              fileName = model.get("fileName");
              break;
            }
          }
        }

        if (fileName) {
          var possibleLocations = [
            ".entitydetails [data-object-name='" + fileName + "']",
            ".entitydetails .control-label:contains('Object Name') + .controls-well:contains('" +
              fileName +
              "')",
            ".entitydetails .control-label:contains('Entity Name') + .controls-well:contains('" +
              fileName +
              "')",
          ];

          //Search through each possible location in the DOM where the file name might be
          for (var i = 0; i < possibleLocations.length; i++) {
            //Get the elements in this view that match the possible location
            var matches = this.$(possibleLocations[i]);

            //If exactly one match is found
            if (matches.length == 1) {
              //Get the entity details parent element
              container = $(matches).parents(".entitydetails").first();
              //Set the object ID on the element for easier locating later
              container.attr("data-id", id);
              if (container.length) break;
            }
          }

          if (container.length) return container;
        }

        //--- The last option:----
        //If this package has only one item, we can assume the only entity details are about that item
        var members = this.packageModels[0].get("members"),
          dataMembers = _.filter(members, function (m) {
            return m.get("formatType") == "DATA";
          });
        if (dataMembers.length == 1) {
          if (this.$(".entitydetails").length == 1) {
            this.$(".entitydetails").attr("data-id", id);
            return this.$(".entitydetails");
          }
        }

        return false;
      },

      /*
       * Inserts new image elements into the DOM via the image template. Use for displaying images that are part of this metadata's resource map.
       */
      insertDataDetails: function () {
        //If there is a metadataIndex subview, render from there.
        var metadataFromIndex = _.findWhere(this.subviews, {
          type: "MetadataIndex",
        });
        if (typeof metadataFromIndex !== "undefined") {
          _.each(this.packageModels, function (packageModel) {
            metadataFromIndex.insertDataDetails(packageModel);
          });
          return;
        }

        var viewRef = this;

        _.each(this.packageModels, function (packageModel) {
          var dataDisplay = "",
            images = [],
            other = [],
            packageMembers = packageModel.get("members");

          //Don't do this for large packages
          if (packageMembers.length > 150) return;

          //==== Loop over each visual object and create a dataDisplay template for it to attach to the DOM ====
          _.each(packageMembers, function (solrResult, i) {
            //Don't display any info about nested packages
            if (solrResult.type == "Package") return;

            var objID = solrResult.get("id");

            if (objID == viewRef.pid) return;

            //Is this a visual object (image)?
            var type =
              solrResult.type == "SolrResult"
                ? solrResult.getType()
                : "Data set";
            if (type == "image") images.push(solrResult);

            //Find the part of the HTML Metadata view that describes this data object
            var anchor = $(document.createElement("a")).attr(
                "id",
                objID.replace(/[^A-Za-z0-9]/g, "-"),
              ),
              container = viewRef.findEntityDetailsContainer(objID);

            var downloadButton = new DownloadButtonView({ model: solrResult });
            downloadButton.render();

            //Insert the data display HTML and the anchor tag to mark this spot on the page
            if (container) {
              //Only show data displays for images hosted on the same origin
              if (type == "image") {
                //Create the data display HTML
                var dataDisplay = $.parseHTML(
                  viewRef
                    .dataDisplayTemplate({
                      type: type,
                      src: solrResult.get("url"),
                      objID: objID,
                    })
                    .trim(),
                );

                //Insert into the page
                if ($(container).children("label").length > 0)
                  $(container).children("label").first().after(dataDisplay);
                else $(container).prepend(dataDisplay);

                //If this image is private, we need to load it via an XHR request
                if (!solrResult.get("isPublic")) {
                  //Create an XHR
                  var xhr = new XMLHttpRequest();
                  xhr.withCredentials = true;

                  xhr.onload = function () {
                    if (xhr.response)
                      $(dataDisplay)
                        .find("img")
                        .attr("src", window.URL.createObjectURL(xhr.response));
                  };

                  //Open and send the request with the user's auth token
                  xhr.open("GET", solrResult.get("url"));
                  xhr.responseType = "blob";
                  xhr.setRequestHeader(
                    "Authorization",
                    "Bearer " + MetacatUI.appUserModel.get("token"),
                  );
                  xhr.send();
                }
              }

              $(container).prepend(anchor);

              var nameLabel = $(container).find(
                "label:contains('Entity Name')",
              );
              if (nameLabel.length) {
                $(nameLabel).parent().after(downloadButton.el);
              }
            }
          });

          //==== Initialize the fancybox images =====
          // We will be checking every half-second if all the HTML has been loaded into the DOM - once they are all loaded, we can initialize the lightbox functionality.
          var numImages = images.length,
            //The shared lightbox options for both images
            lightboxOptions = {
              prevEffect: "elastic",
              nextEffect: "elastic",
              closeEffect: "elastic",
              openEffect: "elastic",
              aspectRatio: true,
              closeClick: true,
              afterLoad: function () {
                //Create a custom HTML caption based on data stored in the DOM element
                viewRef.title =
                  viewRef.title +
                  " <a href='" +
                  viewRef.href +
                  "' class='btn' target='_blank'>Download</a> ";
              },
              helpers: {
                title: {
                  type: "outside",
                },
              },
            };

          if (numImages > 0) {
            var numImgChecks = 0, //Keep track of how many interval checks we have so we don't wait forever for images to load
              lightboxImgSelector =
                "a[class^='fancybox'][data-fancybox-type='image']";

            //Add additional options for images
            var imgLightboxOptions = lightboxOptions;
            imgLightboxOptions.type = "image";
            imgLightboxOptions.perload = 1;

            var initializeImgLightboxes = function () {
              numImgChecks++;

              //Initialize what images have loaded so far after 5 seconds
              if (numImgChecks == 10) {
                $(lightboxImgSelector).fancybox(imgLightboxOptions);
              }
              //When 15 seconds have passed, stop checking so we don't blow up the browser
              else if (numImgChecks > 30) {
                $(lightboxImgSelector).fancybox(imgLightboxOptions);
                window.clearInterval(imgIntervalID);
                return;
              }

              //Are all of our images loaded yet?
              if (viewRef.$(lightboxImgSelector).length < numImages) return;
              else {
                //Initialize our lightboxes
                $(lightboxImgSelector).fancybox(imgLightboxOptions);

                //We're done - clear the interval
                window.clearInterval(imgIntervalID);
              }
            };

            var imgIntervalID = window.setInterval(
              initializeImgLightboxes,
              500,
            );
          }
        });
      },

      replaceEcoGridLinks: function () {
        var viewRef = this;

        //Find the element in the DOM housing the ecogrid link
        $("a:contains('ecogrid://')").each(function (i, thisLink) {
          //Get the link text
          var linkText = $(thisLink).text();

          //Clean up the link text
          var withoutPrefix = linkText.substring(
              linkText.indexOf("ecogrid://") + 10,
            ),
            pid = withoutPrefix.substring(withoutPrefix.indexOf("/") + 1),
            baseUrl =
              MetacatUI.appModel.get("resolveServiceUrl") ||
              MetacatUI.appModel.get("objectServiceUrl");

          $(thisLink)
            .attr("href", baseUrl + encodeURIComponent(pid))
            .text(pid);
        });
      },

      publish: function (event) {
        // target may not actually prevent click events, so double check
        var disabled = $(event.target).closest("a").attr("disabled");
        if (disabled) {
          return false;
        }
        var publishServiceUrl = MetacatUI.appModel.get("publishServiceUrl");
        var pid = $(event.target).closest("a").attr("pid");
        var ret = confirm(
          "Are you sure you want to publish " + pid + " with a DOI?",
        );

        if (ret) {
          // show the loading icon
          var message = "Publishing package...this may take a few moments";
          this.showLoading(message);

          var identifier = null;
          var viewRef = this;
          var requestSettings = {
            url: publishServiceUrl + pid,
            type: "PUT",
            xhrFields: {
              withCredentials: true,
            },
            success: function (data, textStatus, xhr) {
              // the response should have new identifier in it
              identifier = $(data).find("d1\\:identifier, identifier").text();

              if (identifier) {
                viewRef.hideLoading();
                var msg =
                  "Published data package '" +
                  identifier +
                  "'. If you are not redirected soon, you can view your <a href='" +
                  MetacatUI.root +
                  "/view/" +
                  encodeURIComponent(identifier) +
                  "'>published data package here</a>";
                viewRef.$el.find(".container").prepend(
                  viewRef.alertTemplate({
                    msg: msg,
                    classes: "alert-success",
                  }),
                );

                // navigate to the new view after a few seconds
                setTimeout(function () {
                  // avoid a double fade out/in
                  viewRef.$el.html("");
                  viewRef.showLoading();
                  MetacatUI.uiRouter.navigate("view/" + identifier, {
                    trigger: true,
                  });
                }, 3000);
              }
            },
            error: function (xhr, textStatus, errorThrown) {
              // show the error message, but stay on the same page
              var msg =
                "Publish failed: " +
                $(xhr.responseText).find("description").text();

              viewRef.hideLoading();
              viewRef.showError(msg);
            },
          };

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

      //When the given ID from the URL is a resource map that has no metadata, do the following...
      noMetadata: function (solrResultModel) {
        this.hideLoading();
        this.$el.html(this.template());

        this.pid =
          solrResultModel.get("resourceMap") || solrResultModel.get("id");

        //Insert breadcrumbs
        this.insertBreadcrumbs();

        this.insertDataSource();

        //Insert a table of contents
        this.insertPackageTable(solrResultModel);

        this.renderMetadataFromIndex();

        //Insert a message that this data is not described by metadata
        MetacatUI.appView.showAlert(
          "Additional information about this data is limited since metadata was not provided by the creator.",
          "alert-warning",
          this.$(this.metadataContainer),
        );
      },

      // this will lookup the latest version of the PID
      showLatestVersion: function () {
        //If this metadata doc is not obsoleted by a new version, then exit the function
        if (!this.model.get("obsoletedBy")) {
          return;
        }

        var view = this;

        //When the latest version is found,
        this.listenTo(this.model, "change:newestVersion", function () {
          //Make sure it has a newer version, and if so,
          if (view.model.get("newestVersion") != view.model.get("id")) {
            //Put a link to the newest version in the content
            view.$(".newer-version").replaceWith(
              view.versionTemplate({
                pid: view.model.get("newestVersion"),
              }),
            );
          } else {
            view.$(".newer-version").remove();
          }
        });

        //Insert the newest version template with a loading message
        this.$el.prepend(
          this.versionTemplate({
            loading: true,
          }),
        );

        //Find the latest version of this metadata object
        this.model.findLatestVersion();
      },

      showLoading: function (message) {
        this.hideLoading();

        MetacatUI.appView.scrollToTop();

        var loading = this.loadingTemplate({ msg: message });
        if (!loading) return;

        this.$loading = $($.parseHTML(loading));
        this.$detached = this.$el.children().detach();

        this.$el.html(loading);
      },

      hideLoading: function () {
        if (this.$loading) this.$loading.remove();
        if (this.$detached) this.$el.html(this.$detached);
      },

      showError: function (msg) {
        //Remove any existing error messages
        this.$el.children(".alert-container").remove();

        this.$el.prepend(
          this.alertTemplate({
            msg: msg,
            classes: "alert-error",
            containerClasses: "page",
            includeEmail: true,
          }),
        );
      },

      /**
       * When the "Metadata" button in the table is clicked while we are on the Metadata view,
       * we want to scroll to the anchor tag of this data object within the page instead of navigating
       * to the metadata page again, which refreshes the page and re-renders (more loading time)
       **/
      previewData: function (e) {
        //Don't go anywhere yet...
        e.preventDefault();

        //Get the target and id of the click
        var link = $(e.target);
        if (!$(link).hasClass("preview")) link = $(link).parents("a.preview");

        if (link) {
          var id = $(link).attr("data-id");
          if (typeof id === "undefined" || !id) return false; //This will make the app defualt to the child view previewData function
        } else return false;

        // If we are on the Metadata view, update the  URL and scroll to the
        // anchor
        window.location.hash = encodeURIComponent(id);
        MetacatUI.appView.scrollTo(this.findEntityDetailsContainer(id));

        return true;
      },

      /**
       * Try to scroll to the section on a page describing the identifier in the
       * fragment/hash portion of the current page.
       *
       * This function depends on there being an `id` dataset attribute on an
       * element on the page set to an XML-safe version of the value in the
       * fragment/hash. Used to provide direct links to sub-resources on a page.
       */
      scrollToFragment: function () {
        var hash = window.location.hash;

        if (!hash || hash.length <= 1) {
          return;
        }

        //Get the id from the URL hash and decode it
        var idFragment = decodeURIComponent(hash.substring(1));

        //Find the corresponding entity details section for this id
        var entityDetailsEl = this.findEntityDetailsContainer(idFragment);

        if (entityDetailsEl || entityDetailsEl.length) {
          MetacatUI.appView.scrollTo(entityDetailsEl);
        }
      },

      /**
       * Navigate to a new /view URL with a fragment
       *
       * Used in getModel() when the pid originally passed into MetadataView
       * is not a metadata PID but is, instead, a data PID. getModel() does
       * the work of finding an appropriate metadata PID for the data PID and
       * this method handles re-routing to the correct URL.
       *
       * @param {string} metadata_pid - The new metadata PID
       * @param {string} data_pid - Optional. A data PID that's part of the
       *   package metadata_pid exists within.
       */
      navigateWithFragment: function (metadata_pid, data_pid) {
        var next_route = "view/" + encodeURIComponent(metadata_pid);

        if (typeof data_pid === "string" && data_pid.length > 0) {
          next_route += "#" + encodeURIComponent(data_pid);
        }

        MetacatUI.uiRouter.navigate(next_route, { trigger: true });
      },

      closePopovers: function (e) {
        //If this is a popover element or an element that has a popover, don't close anything.
        //Check with the .classList attribute to account for SVG elements
        var svg = $(e.target).parents("svg");

        if (
          _.contains(e.target.classList, "popover-this") ||
          $(e.target).parents(".popover-this").length > 0 ||
          $(e.target).parents(".popover").length > 0 ||
          _.contains(e.target.classList, "popover") ||
          (svg.length && _.contains(svg[0].classList, "popover-this"))
        )
          return;

        //Close all active popovers
        this.$(".popover-this.active").popover("hide");
      },

      highlightNode: function (e) {
        //Find the id
        var id = $(e.target).attr("data-id");

        if (typeof id === "undefined" || !id)
          id = $(e.target).parents("[data-id]").attr("data-id");

        //If there is no id, return
        if (typeof id === "undefined") return false;

        //Highlight its node
        $(".prov-chart .node[data-id='" + id + "']").toggleClass("active");

        //Highlight its metadata section
        if (MetacatUI.appModel.get("pid") == id)
          this.$("#Metadata").toggleClass("active");
        else {
          var entityDetails = this.findEntityDetailsContainer(id);
          if (entityDetails) entityDetails.toggleClass("active");
        }
      },

      onClose: function () {
        var viewRef = this;

        this.stopListening();

        _.each(this.subviews, function (subview) {
          if (subview.onClose) subview.onClose();
        });

        this.packageModels = new Array();
        this.model.set(this.model.defaults);
        this.pid = null;
        this.dataPackage = null;
        this.seriesId = null;
        this.$detached = null;
        this.$loading = null;

        //Put the document title back to the default
        MetacatUI.appModel.resetTitle();

        //Remove view-specific classes
        this.$el.removeClass("container no-stylesheet");

        this.$el.empty();
      },

      /**
       * Generate a string appropriate to go into the author/creator portion of
       * a dataset citation from the value stored in the underlying model's
       * origin field.
       */
      getAuthorText: function () {
        var authors = this.model.get("origin"),
          count = 0,
          authorText = "";

        _.each(authors, function (author) {
          count++;

          if (count == 6) {
            authorText += ", et al. ";
            return;
          } else if (count > 6) {
            return;
          }

          if (count > 1) {
            if (authors.length > 2) {
              authorText += ",";
            }

            if (count == authors.length) {
              authorText += " and";
            }

            if (authors.length > 1) {
              authorText += " ";
            }
          }

          authorText += author;
        });

        return authorText;
      },

      /**
       * Generate a string appropriate to be used in the publisher portion of a
       * dataset citation. This method falls back to the node ID when the proper
       * node name cannot be fetched from the app's NodeModel instance.
       */
      getPublisherText: function () {
        var datasource = this.model.get("datasource"),
          memberNode = MetacatUI.nodeModel.getMember(datasource);

        if (memberNode) {
          return memberNode.name;
        } else {
          return datasource;
        }
      },

      /**
       * Generate a string appropriate to be used as the publication date in a
       * dataset citation.
       */
      getDatePublishedText: function () {
        // Dataset/datePublished
        // Prefer pubDate, fall back to dateUploaded so we have something to show
        if (this.model.get("pubDate") !== "") {
          return this.model.get("pubDate");
        } else {
          return this.model.get("dateUploaded");
        }
      },

      /**
       * Generate Schema.org-compliant JSONLD for the model bound to the view into
       *  the head tag of the page by `insertJSONLD`.
       *
       * Note: `insertJSONLD` should be called to do the actual inserting into the
       * DOM.
       */
      generateJSONLD: function () {
        var model = this.model;

        // Determine the path (either #view or view, depending on router
        // configuration) for use in the 'url' property
        var href = document.location.href,
          route = href
            .replace(document.location.origin + "/", "")
            .split("/")[0];

        // First: Create a minimal Schema.org Dataset with just the fields we
        // know will come back from Solr (System Metadata fields).
        // Add the rest in conditional on whether they are present.
        var elJSON = {
          "@context": {
            "@vocab": "https://schema.org/",
          },
          "@type": "Dataset",
          "@id":
            "https://dataone.org/datasets/" +
            encodeURIComponent(model.get("id")),
          datePublished: this.getDatePublishedText(),
          dateModified: model.get("dateModified"),
          publisher: {
            "@type": "Organization",
            name: this.getPublisherText(),
          },
          identifier: this.generateSchemaOrgIdentifier(model.get("id")),
          version: model.get("version"),
          url:
            "https://dataone.org/datasets/" +
            encodeURIComponent(model.get("id")),
          schemaVersion: model.get("formatId"),
          isAccessibleForFree: true,
        };

        // Attempt to add in a sameAs property of we have high confidence the
        // identifier is a DOI
        if (this.model.isDOI(model.get("id"))) {
          var doi = this.getCanonicalDOIIRI(model.get("id"));

          if (doi) {
            elJSON["sameAs"] = doi;
          }
        }

        // Second: Add in optional fields

        // Name
        if (model.get("title")) {
          elJSON["name"] = model.get("title");
        }

        // Creator
        if (model.get("origin")) {
          elJSON["creator"] = model.get("origin").map(function (creator) {
            return {
              "@type": "Person",
              name: creator,
            };
          });
        }

        // Dataset/spatialCoverage
        if (
          model.get("northBoundCoord") &&
          model.get("eastBoundCoord") &&
          model.get("southBoundCoord") &&
          model.get("westBoundCoord")
        ) {
          var spatialCoverage = {
            "@type": "Place",
            additionalProperty: [
              {
                "@type": "PropertyValue",
                additionalType:
                  "http://dbpedia.org/resource/Coordinate_reference_system",
                name: "Coordinate Reference System",
                value: "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
              },
            ],
            geo: this.generateSchemaOrgGeo(
              model.get("northBoundCoord"),
              model.get("eastBoundCoord"),
              model.get("southBoundCoord"),
              model.get("westBoundCoord"),
            ),
            subjectOf: {
              "@type": "CreativeWork",
              fileFormat: "application/vnd.geo+json",
              text: this.generateGeoJSONString(
                model.get("northBoundCoord"),
                model.get("eastBoundCoord"),
                model.get("southBoundCoord"),
                model.get("westBoundCoord"),
              ),
            },
          };

          elJSON.spatialCoverage = spatialCoverage;
        }

        // Dataset/temporalCoverage
        if (model.get("beginDate") && !model.get("endDate")) {
          elJSON.temporalCoverage = model.get("beginDate");
        } else if (model.get("beginDate") && model.get("endDate")) {
          elJSON.temporalCoverage =
            model.get("beginDate") + "/" + model.get("endDate");
        }

        // Dataset/variableMeasured
        if (model.get("attributeName")) {
          elJSON.variableMeasured = model.get("attributeName");
        }

        // Dataset/description
        if (model.get("abstract")) {
          elJSON.description = model.get("abstract");
        } else {
          var datasets_url =
            "https://dataone.org/datasets/" +
            encodeURIComponent(model.get("id"));
          elJSON.description =
            "No description is available. Visit " +
            datasets_url +
            " for complete metadata about this dataset.";
        }

        // Dataset/keywords
        if (model.get("keywords")) {
          elJSON.keywords = model.get("keywords").join(", ");
        }

        return elJSON;
      },

      /**
       * Insert Schema.org-compliant JSONLD for the model bound to the view into
       * the head tag of the page (at the end).
       *
       * @param {object} json - JSON-LD to insert into the page
       *
       * Some notes:
       *
       * - Checks if the JSONLD already exists from the previous data view
       * - If not create a new script tag and append otherwise replace the text
       *   for the script
       */
      insertJSONLD: function (json) {
        if (!document.getElementById("jsonld")) {
          var el = document.createElement("script");
          el.type = "application/ld+json";
          el.id = "jsonld";
          el.text = JSON.stringify(json);
          document.querySelector("head").appendChild(el);
        } else {
          var script = document.getElementById("jsonld");
          script.text = JSON.stringify(json);
        }
      },

      /**
       * Generate a Schema.org/identifier from the model's id
       *
       * Tries to use the PropertyValue pattern when the identifier is a DOI
       * and falls back to a Text value otherwise
       *
       * @param {string} identifier - The raw identifier
       */
      generateSchemaOrgIdentifier: function (identifier) {
        if (!this.model.isDOI()) {
          return identifier;
        }

        var doi = this.getCanonicalDOIIRI(identifier);

        if (!doi) {
          return identifier;
        }

        return {
          "@type": "PropertyValue",
          propertyID: "https://registry.identifiers.org/registry/doi",
          value: doi.replace("https://doi.org/", "doi:"),
          url: doi,
        };
      },

      /**
       * Generate a Schema.org/Place/geo from bounding coordinates
       *
       * Either generates a GeoCoordinates (when the north and east coords are
       * the same) or a GeoShape otherwise.
       */
      generateSchemaOrgGeo: function (north, east, south, west) {
        if (north === south) {
          return {
            "@type": "GeoCoordinates",
            latitude: north,
            longitude: west,
          };
        } else {
          return {
            "@type": "GeoShape",
            box: west + ", " + south + " " + east + ", " + north,
          };
        }
      },

      /**
       * Creates a (hopefully) valid geoJSON string from the a set of bounding
       * coordinates from the Solr index (north, east, south, west).
       *
       * This function produces either a GeoJSON Point or Polygon depending on
       * whether the north and south bounding coordinates are the same.
       *
       * Part of the reason for factoring this out, in addition to code
       * organization issues, is that the GeoJSON spec requires us to modify
       * the raw result from Solr when the coverage crosses -180W which is common
       * for datasets that cross the Pacific Ocean. In this case, We need to
       * convert the east bounding coordinate from degrees west to degrees east.
       *
       * e.g., if the east bounding coordinate is 120 W and west bounding
       * coordinate is 140 E, geoJSON requires we specify 140 E as 220
       *
       * @param {number} north - North bounding coordinate
       * @param {number} east - East bounding coordinate
       * @param {number} south - South bounding coordinate
       * @param {number} west - West bounding coordinate
       */
      generateGeoJSONString: function (north, east, south, west) {
        if (north === south) {
          return this.generateGeoJSONPoint(north, east);
        } else {
          return this.generateGeoJSONPolygon(north, east, south, west);
        }
      },

      /**
         * Generate a GeoJSON Point object
         *
         * @param {number} north - North bounding coordinate
         * @param {number} east - East bounding coordinate
         *
         * Example:
         * {
         *  "type": "Point",
         *  "coordinates": [
         *      -105.01621,
         *      39.57422
         * ]}

        */
      generateGeoJSONPoint: function (north, east) {
        var preamble = '{"type":"Point","coordinates":',
          inner = "[" + east + "," + north + "]",
          postamble = "}";

        return preamble + inner + postamble;
      },

      /**
       * Generate a GeoJSON Polygon object from
       *
       * @param {number} north - North bounding coordinate
       * @param {number} east - East bounding coordinate
       * @param {number} south - South bounding coordinate
       * @param {number} west - West bounding coordinate
       *
       *
       * Example:
       *
       * {
       *   "type": "Polygon",
       *   "coordinates": [[
       *     [ 100, 0 ],
       *     [ 101, 0 ],
       *     [ 101, 1 ],
       *     [ 100, 1 ],
       *     [ 100, 0 ]
       * ]}
       *
       */
      generateGeoJSONPolygon: function (north, east, south, west) {
        var preamble =
          '{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[';

        // Handle the case when the polygon wraps across the 180W/180E boundary
        if (east < west) {
          east = 360 - east;
        }

        var inner =
          "[" +
          west +
          "," +
          south +
          "]," +
          "[" +
          east +
          "," +
          south +
          "]," +
          "[" +
          east +
          "," +
          north +
          "]," +
          "[" +
          west +
          "," +
          north +
          "]," +
          "[" +
          west +
          "," +
          south +
          "]";

        var postamble = "]]}}";

        return preamble + inner + postamble;
      },

      /**
       * Create a canonical IRI for a DOI given a random DataONE identifier.
       *
       * @param {string} identifier: The identifier to (possibly) create the IRI
       *   for.
       * @return {string|null} Returns null when matching the identifier to a DOI
       *   regex fails or a string when the match is successful
       *
       * Useful for describing resources identified by DOIs in linked open data
       * contexts or possibly also useful for comparing two DOIs for equality.
       *
       * Note: Really could be generalized to more identifier schemes.
       */
      getCanonicalDOIIRI: function (identifier) {
        return MetacatUI.appModel.DOItoURL(identifier) || null;
      },

      /**
       * Insert citation information as meta tags into the head of the page
       *
       * Currently supports Highwire Press style tags (citation_) which is
       * supposedly what Google (Scholar), Mendeley, and Zotero support.
       */
      insertCitationMetaTags: function () {
        // Generate template data to use for all templates
        var title = this.model.get("title"),
          authors = this.model.get("origin"),
          publisher = this.getPublisherText(),
          date = new Date(this.getDatePublishedText())
            .getUTCFullYear()
            .toString(),
          isDOI = this.model.isDOI(this.model.get("id")),
          id = this.model.get("id"),
          abstract = this.model.get("abstract");

        // Generate HTML strings from each template
        var hwpt = this.metaTagsHighwirePressTemplate({
          title: title,
          authors: authors,
          publisher: publisher,
          date: date,
          isDOI: isDOI,
          id: id,
          abstract,
        });

        // Clear any that are already in the document.
        $("meta[name='citation_title']").remove();
        $("meta[name='citation_authors']").remove();
        $("meta[name='citation_author']").remove();
        $("meta[name='citation_publisher']").remove();
        $("meta[name='citation_date']").remove();
        $("meta[name='citation_doi']").remove();
        $("meta[name='citation_abstract']").remove();

        // Insert
        document.head.insertAdjacentHTML("beforeend", hwpt);

        // Update Zotero
        // https://www.zotero.org/support/dev/exposing_metadata#force_zotero_to_refresh_metadata
        document.dispatchEvent(
          new Event("ZoteroItemUpdated", {
            bubbles: true,
            cancelable: true,
          }),
        );
      },

      createAnnotationViews: function () {
        try {
          var viewRef = this;

          _.each($(".annotation"), function (annoEl) {
            var newView = new AnnotationView({
              el: annoEl,
            });
            viewRef.subviews.push(newView);
          });
        } catch (e) {
          console.error(e);
        }
      },

      insertMarkdownViews: function () {
        var viewRef = this;

        _.each($(".markdown"), function (markdownEl) {
          var newView = new MarkdownView({
            markdown: $(markdownEl).text().trim(),
            el: $(markdownEl).parent(),
          });

          viewRef.subviews.push(newView);

          // Clear out old content before rendering
          $(markdownEl).remove();

          newView.render();
        });
      },

      storeEntityPIDs: function (responseEl) {
        var view = this;
        _.each($(responseEl).find(".entitydetails"), function (entityEl) {
          var entityId = $(entityEl).data("id");
          view.entities.push(entityId.replace("urn-uuid-", "urn:uuid:"));
        });
      },
    },
  );

  return MetadataView;
});