Source: 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',
    'views/DownloadButtonView',
    'views/ProvChartView',
    'views/MetadataIndexView',
    'views/ExpandCollapseListView',
    'views/ProvStatementView',
    'views/PackageTableView',
    'views/CitationView',
    '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, DownloadButtonView, ProvChart, MetadataIndex, ExpandCollapseList, ProvStatement, PackageTable,
       CitationView, 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
 * @extends Backbone.View
 * @constructor
 */
  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",
    ownerControlsContainer: "#owner-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;
    },

    getDataPackage: function(pid) {
            // Get the metadata model that is associated with this DataPackage collection
            //var metadataModel = new ScienceMetadata({ id: this.pid });
            // Once the ScienceMetadata is populated, populate the associated package
            //this.metadataModel = metadataModel;

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

      //Fetch the data package. DataPackage.parse() triggers 'complete'
      this.dataPackage.fetch({
        fetchModels: false
      });

      this.listenToOnce(this.dataPackage, "complete", function() {
        // parseProv triggers "queryComplete"
        this.dataPackage.parseProv();
        this.checkForProv();

        var packageTableView = _.findWhere(this.subviews, { type: "PackageTable"});
        if( packageTableView ){
          packageTableView.dataPackageCollection = this.dataPackage;
          packageTableView.checkForPrivateMembers();
        }
      });
    },
    /*
     * 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.pid = _.first(isDocBy);
            this.getModel(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",
                    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.pid = latestVersions[0];
                  view.getModel(latestVersions[0]);
                }
                //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{
                  var fallbackPid =  _.last(isDocBy);
                  view.pid = fallbackPid;
                  view.getModel(fallbackPid);
                }

              });

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

      // Edit button and the publish button
      this.insertOwnerControls();

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

                viewRef.setUpAnnotator();
              }
            },
            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();

        // render annotator from index content, too
        view.setUpAnnotator();
      });
    },

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

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

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

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

    /*
    * 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"));
              });
              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 DataPackge info in order to render prov extraced from the resmap.
      if(packages.length) this.getDataPackage(packages[0].get("id"));

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

        var url = "https://maps.google.com/?ll=" + latLngCEN.lat() + "," + latLngCEN.lng() +
              "&spn=0.003833,0.010568" +
              "&t=m" +
              "&z=10";
        //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=color:0xDA4D3Aff|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()+
                "&zoom=4" +
                "&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 element from the model attributes
      var citation = new CitationView({
                  model: this.model,
                  createLink: false }).render().el;
      this.$(this.citationContainer).html(citation);
    },

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

        //Insert the data source template
        this.$(this.dataSourceContainer).html(this.dataSourceTemplate({
          node : dataSource
        })).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="https://search.dataone.org/profile/' +
                        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);
          });

      }
    },

    /*
     * Checks the authority for the logged in user for this dataset
     * and inserts control elements onto the page for the user to interact with the dataset - edit, publish, etc.
     */
    insertOwnerControls: function(){

      //Do not show user controls for older versions of data sets
      if(this.model.get("obsoletedBy") && (this.model.get("obsoletedBy").length > 0))
        return false;

      var container = this.$(this.ownerControlsContainer);

      //Save some references
      var pid     = this.model.get("id") || this.pid,
        model   = this.model,
        viewRef = this;

      this.listenToOnce(this.model, "change:isAuthorized", function(){
        if(!model.get("isAuthorized") || model.get("archived"))
          return false;

        //Insert an Edit button
        if( _.contains(MetacatUI.appModel.get("editableFormats"), this.model.get("formatId")) ){
          container.append(
            this.editMetadataTemplate({
              identifier: pid,
              supported: true
            }));
        }
        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") && !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(
              viewRef.doiTemplate({
                isAuthorized: true,
                identifier: pid
              }));
          }
        }
        catch(e){
          console.error("Cannot display the publish button: ", e);
        }

        //Check the authority on the package models
        //If there is no package, then exit now
        if(!viewRef.packageModels || !viewRef.packageModels.length)
          return;

        //Check for authorization on the resource map
        var packageModel = this.packageModels[0];

        //if there is no package, then exit now
        if(!packageModel.get("id")) return;

        //Now get the RDF XML and check for the user's authority on this resource map
        packageModel.fetch();
        packageModel.checkAuthority();
      });

      //Check if the current user has authority to `changePermission` on this metadata
      this.model.checkAuthority();
    },

    /*
     * 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({
          citation: $(this.citationContainer).text(),
          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);

      var view = this;

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

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

      //Create clickable "Copy" buttons to copy text (e.g. citation) to the user's clipboard
      var copyBtns = $(this.controlsContainer).find(".copy");
      _.each(copyBtns, function(btn){
        //Create a copy citation button

        var clipboard = new Clipboard(btn);

        clipboard.on("success", function(e){

          var originalWidth = $(e.trigger).width();

          $(e.trigger).html( $(document.createElement("span")).addClass("icon icon-ok success") )
                      .append(" Copied")
                      .addClass("success")
                      .css("width", originalWidth + "px");

          setTimeout(function() {
            $(e.trigger).html("<i class='icon icon-copy icon-on-left'></i> Copy Citation")
                        .removeClass("success")
                        .css("width", "auto");
          }, 500)

        });

        clipboard.on("error", function(e){

          if(!$(e.trigger).prev("input.copy").length){
            var textarea = $(document.createElement("input")).val($(e.trigger).attr("data-clipboard-text")).addClass("copy").css("width", "0");
            textarea.tooltip({
              title: "Press Ctrl+c to copy",
              placement: "top"
            });
            $(e.trigger).before(textarea);
          }
          else{
            var textarea = $(e.trigger).prev("input.copy");
          }

          textarea.animate({ width: "100px" }, {
            duration: "slow",
            complete: function(){
              textarea.trigger("focus");
              textarea.tooltip("show");
            }
          });

          textarea.focusout(function(){
            textarea.animate({ width: "0px" }, function(){
              textarea.remove();
            })
          });
        });
      });
      this.$(".tooltip-this").tooltip();
    },

    /**
     *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("displayDatasetCitationMetric")) {
          var citationsMetricView = new MetricView({metricName: 'Citations', model: metricsModel});
          buttonToolbar.append(citationsMetricView.render().el);
        }

        if (MetacatUI.appModel.get("displayDatasetDownloadMetric")) {
          var downloadsMetricView = new MetricView({metricName: 'Downloads', model: metricsModel});
          buttonToolbar.append(downloadsMetricView.render().el);
        }

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

      }


    /*        if(MetacatUI.appModel.get("displayDatasetControls")) {
                var controlsToolbar = $(document.createElement("div")).addClass("edit-toolbar btn-toolbar");
                var copyCitationToolbar = this.$(this.controlsContainer);

                //Insert controls
                this.insertControls();
                controlsToolbar.append(copyCitationToolbar)

                if(MetacatUI.appModel.get("displayDatasetEditButton")) {
                    var editToolbar = this.$(this.ownerControlsContainer);

                    // Insert Owner Controls
                    this.insertOwnerControls();
                    controlsToolbar.append(editToolbar)
                }

        metrics.append(controlsToolbar);
            }
*/

    },


        // Check if the DataPackage provenance parsing has completed.
        checkForProv: function() {
            // Show the provenance trace for this package
            var model = this.model;
            if(this.dataPackage.provenanceFlag == "complete") {
                this.drawProvCharts(this.dataPackage);
                // Check each prov chart to see if it has been marked for re-rendering and
                // redraw it if it has been.
                this.listenToOnce(this.dataPackage, "redrawProvCharts", this.redrawProvCharts);
                this.model.once("change:isAuthorized", this.redrawProvCharts, this);
            } else {
                this.listenToOnce(this.dataPackage, "queryComplete", function() {
                    this.drawProvCharts(this.dataPackage);
                    // Check each prov chart to see if it has been marked for re-rendering and
                    // redraw it if it has been.
                    this.listenToOnce(this.dataPackage, "redrawProvCharts", this.redrawProvCharts);
                    model.once("change:isAuthorized", this.redrawProvCharts, this);
                });
            }
        },

    /*
     * 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){
      //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 isAuthorized = true;
      var editModeOn = false;

      //If the user is authorized to edit this metadata doc, then turn edit mode on
      this.model.get("isAuthorized") ? editModeOn = true : editModeOn = false;
      //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);
      view.listenToOnce(this.dataPackage, "redrawProvCharts", view.redrawProvCharts);

    },

        /*
         * 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.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){
            console.log("saveError called");
            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 = [],
          pdfs = [],
          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 or PDF)?
          var type = solrResult.type == "SolrResult" ? solrResult.getType() : "Data set";
          if(type == "image")
            images.push(solrResult);
          else if(type == "PDF")
            pdfs.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 and PDFs hosted on the same origin
            if((type == "image") || ((type == "PDF") && solrResult.get("url").indexOf(window.location.host) > -1) ){

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

                if(type == "PDF"){
                  //When the XHR is ready, create a link with the raw data (Blob) and click the link to download
                  xhr.onload = function(){
                      var iframe = $(dataDisplay).find("iframe");
                      iframe.attr("src", window.URL.createObjectURL(xhr.response)); // xhr.response is a blob
                      var a = $(dataDisplay).find("a.zoom-in").remove();
                      //TODO: Allow fancybox previews of private PDFs

                  }
                }
                else if(type == "image"){
                  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,
          numPDFS     = pdfs.length,
          //The shared lightbox options for both images and PDFs
          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(numPDFS > 0){
          var numPDFChecks  = 0,
            lightboxPDFSelector = "a[class^='fancybox'][data-fancybox-iframe]";

          //Add additional options for PDFs
          var pdfLightboxOptions = lightboxOptions;
          pdfLightboxOptions.type = "iframe";
          pdfLightboxOptions.iframe = { preload: false };
          pdfLightboxOptions.height = "98%";

          var initializePDFLightboxes = function(){
            numPDFChecks++;

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

            //Are all of our pdfs loaded yet?
            if(viewRef.$(lightboxPDFSelector).length < numPDFS) return;
            else{
              //Initialize our lightboxes
              $(lightboxPDFSelector).fancybox(pdfLightboxOptions);

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

          var pdfIntervalID = window.setInterval(initializePDFLightboxes, 500);
        }

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

    /*
     * Inserts new image elements into the DOM via the image template. Use for displaying images that are part of this metadata's resource map.
     * param pdfs - an array of objects that represent the data objects returned from the index. Each should be a PDF
     */
    insertPDFs: function(pdfs){
      var html = "",
       viewRef = this;

      //Loop over each image object and create a dataDisplay template for it to attach to the DOM
      for(var i=0; i<pdfs.length; i++){
        //Find the part of the HTML Metadata view that describes this data object
        var container = this.$el.find("td:contains('" + pdfs[i].id + "')").parents(".controls-well");

        //Harvest the Object Name for an image caption
        if(container !== undefined) var title = container.find("label:contains('Object Name')").next().text();
        else{
          var title = "";
          container = viewRef.el;
        }
        //Create an element using the dataDisplay template
        html = this.dataDisplayTemplate({
           type : "pdf",
            src : (MetacatUI.appModel.get('objectServiceUrl') || MetacatUI.appModel.get('resolveServiceUrl')) + pdfs[i].id,
          title : title
        });

        // Insert the element into the DOM
        $(container).append(html);
      }

      //==== Initialize the fancybox images =====
      // We will be checking every half-second if all the images have been loaded into the DOM - once they are all loaded, we can initialize the lightbox functionality.
      var numPDFs  = pdfs.length,
        numChecks  = 0, //Keep track of how many interval checks we have so we don't wait forever for images to load
        lightboxSelector = "a[class^='fancybox'][data-fancybox-iframe]",
        intervalID = window.setInterval(initializeLightboxes, 500);

      //Set up our lightbox options
      var lightboxOptions = {
          prevEffect  : 'elastic',
          nextEffect  : 'elastic',
          closeEffect : 'elastic',
          openEffect  : 'elastic',
          type     : "iframe",
          aspectRatio : true,
          helpers      : {
                title : {
                    type : 'outside'
                }
          },
           iframe    : {
                 preload : false
           },
           closeClick : true,
           afterLoad  : function(){
             //Create a custom HTML caption based on data stored in the DOM element
             this.title = this.title + " <a href='" + this.href + "' class='btn' target='_blank'>Download</a> ";
           }
      }

      function initializeLightboxes(){
        numChecks++;

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

        //Are all of our pdfs loaded yet?
        if(viewRef.$(lightboxSelector).length < numPDFs) return;
        else{
          //Initialize our lightboxes
          $(lightboxSelector).fancybox(lightboxOptions);

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

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

    /**
    * If the annotator feature is enabled, this function renders an AnnotatorView.
    * The Annotator feature is experiemental and unsupported, so this should only be enabled by advanced users
    * @deprecated
    */
    setUpAnnotator: function() {
      if(!MetacatUI.appModel.get("annotatorUrl"))
        return;

      var thisView = this;

      require(["views/AnnotatorView"], function(AnnotatorView){
        var annotator = new AnnotatorView({
          parentView: thisView
          });
        thisView.subviews.push(annotator);
        annotator.render();
      });
    },

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

    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": "http://schema.org",
        },
        "@type": "Dataset",
        "@id": "https://dataone.org/datasets/" +
          encodeURIComponent(model.get("id")),
        "datePublished" : this.getDatePublishedText(),
        "publisher": {
          "@type": "Organization",
          "name": this.getPublisherText()
        },
        "identifier": model.get("id"),
        "url": "https://dataone.org/datasets/" +
          encodeURIComponent(model.get("id")),
        "schemaVersion": model.get("formatId"),
      };

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

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

    /**
     * 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.getAuthorText(),
        publisher = this.getPublisherText(),
        date = new Date(this.getDatePublishedText()).getUTCFullYear().toString();

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

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

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

    createAnnotationViews: function(){

      try{
        var viewRef = this;

        _.each($(".annotation"), function (annoEl) {
          var newView = new AnnotationView({
            el: annoEl
          });
          viewRef.subviews.push(newView);
          newView.render();
        });
      }
      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;
});