Source: src/js/views/MetadataView.js

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