/*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(" ") > -1)
coordinate = coordinate.substring(0, coordinate.indexOf(" "));
}
}
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;
});