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