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/DownloadButtonView',
  'views/ProvChartView',
  'views/MetadataIndexView',
  'views/ExpandCollapseListView',
  'views/ProvStatementView',
  'views/PackageTableView',
  '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, DownloadButtonView, ProvChart, MetadataIndex, ExpandCollapseList, ProvStatement, PackageTable,
    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(),
        dataPackage: null,
        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: [],

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

          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("packageTableRendered", function () {
            //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") });

          // 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) {
            this.checkWritePermissions();
            return
          }

          this.listenToOnce(this.dataPackage, "complete", function () {
            var packageTableView = _.findWhere(this.subviews, { type: "PackageTable" });
            if (packageTableView) {
              packageTableView.dataPackageCollection = this.dataPackage;
              packageTableView.checkForPrivateMembers();
            }

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

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

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

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

        /*
         * Inserts a table with all the data package member information and sends the call to display annotations
         */
        insertPackageDetails: function (packages) {

          //Don't insert the package details twice
          var tableEls = this.$(this.tableContainer).children().not(".loading");
          if (tableEls.length > 0) return;

          //wait for the metadata to load
          var metadataEls = this.$(this.metadataContainer).children();
          if (!metadataEls.length || metadataEls.first().is(".loading")) {
            this.once("metadataLoaded", this.insertPackageDetails);
            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 = 'Current Data Set (1 of ' + (nestedPckgsToDisplay.length + 1) + ') <span class="subtle">Package: ' + packageModel.get("id") + '</span>';
                this.insertPackageTable(packageModel, { title: title });
              }

              _.each(nestedPckgsToDisplay, function (nestedPackage, i, list) {
                if (!(!this.model.get("archived") && nestedPackage.get("archived") == true)) {

                  var title = 'Nested Data Set (' + (i + 2) + ' of ' +
                    (list.length + 1) + ') <span class="subtle">Package: ' +
                    nestedPackage.get("id") + '</span> <a href="' + MetacatUI.root +
                    '/view/' + encodeURIComponent(nestedPackage.get("id")) +
                    '" class="table-header-link">(View <i class="icon icon-external-link-sign icon-on-right"></i> ) </a>';

                  this.insertPackageTable(nestedPackage, { title: title, nested: true });

                }
              }, this);
            }
            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>' : "";
                title = "Files in this dataset " + title;
                this.insertPackageTable(packageModel, { title: title });
              }
            }

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

          }, this);

          //Collapse the table list after the first table
          var additionalTables = $(this.$("#additional-tables-for-" + this.cid)),
            numTables = additionalTables.children(".download-contents").length,
            item = (numTables == 1) ? "dataset" : "datasets";
          if (numTables > 0) {
            var expandIcon = $(document.createElement("i")).addClass("icon icon-level-down"),
              expandLink = $(document.createElement("a"))
                .attr("href", "#")
                .addClass("toggle-slide toggle-display-on-slide")
                .attr("data-slide-el", "additional-tables-for-" + this.cid)
                .text("Show " + numTables + " nested " + item)
                .prepend(expandIcon),
              collapseLink = $(document.createElement("a"))
                .attr("href", "#")
                .addClass("toggle-slide toggle-display-on-slide")
                .attr("data-slide-el", "additional-tables-for-" + this.cid)
                .text("Hide nested " + item)
                .hide(),
              expandControl = $(document.createElement("div")).addClass("expand-collapse-control").append(expandLink, collapseLink);

            additionalTables.before(expandControl);
          }

          //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;
            this.insertPackageTable(packageModel);
          }

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

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

          if (typeof packageModel === "undefined") return;

          //** Draw the package table **//
          var tableView = new PackageTable({
            model: packageModel,
            currentlyViewing: this.pid,
            parentView: this,
            title: 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).append(tableView.render().el);
          $(this.tableContainer).children(".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("packageTableRendered");
        },

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

            var url = "https://maps.google.com/?ll=" + latLngCEN.lat() + "," + latLngCEN.lng() +
              "&spn=0.003833,0.010568" +
              "&t=m" +
              "&z=5";

            //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;
            $(insertAfter).append(this.mapTemplate({
              map: mapHTML,
              url: url
            }));

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

          }

          return true;

        },

        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 PackageTableView 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("PackageTable") !== -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.seriesId = null;
          this.$detached = null;
          this.$loading = null;

          //Put the document title back to the default
          MetacatUI.appModel.set("title", MetacatUI.appModel.defaults.title);

          //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) {
          var pattern = /(10\.\d{4,9}\/[-\._;()\/:A-Z0-9]+)$/,
            match = identifier.match(pattern);

          if (match === null || match.length !== 2 || match[1].length <= 0) {
            return null;
          }

          return "https://doi.org/" + match[1];
        },
        /**
             * 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();
          });
        }

      });

    return MetadataView;
  });