/*global define */ define(['jquery', 'jqueryui', 'underscore', 'backbone', 'gmaps', 'fancybox', 'clipboard', 'collections/DataPackage', 'models/DataONEObject', 'models/PackageModel', 'models/SolrResult', 'models/metadata/ScienceMetadata', 'models/MetricsModel', 'common/Utilities', 'views/DownloadButtonView', 'views/ProvChartView', 'views/MetadataIndexView', 'views/ExpandCollapseListView', 'views/ProvStatementView', 'views/PackageTableView', 'views/CitationHeaderView', 'views/citations/CitationModalView', 'views/AnnotationView', 'views/MarkdownView', 'text!templates/metadata/metadata.html', 'text!templates/dataSource.html', 'text!templates/publishDOI.html', 'text!templates/newerVersion.html', 'text!templates/loading.html', 'text!templates/metadataControls.html', 'text!templates/metadataInfoIcons.html', 'text!templates/alert.html', 'text!templates/editMetadata.html', 'text!templates/dataDisplay.html', 'text!templates/map.html', 'text!templates/annotation.html', 'text!templates/metaTagsHighwirePress.html', 'uuid', 'views/MetricView', ], function ($, $ui, _, Backbone, gmaps, fancybox, Clipboard, DataPackage, DataONEObject, Package, SolrResult, ScienceMetadata, MetricsModel, Utilities, DownloadButtonView, ProvChart, MetadataIndex, ExpandCollapseList, ProvStatement, PackageTable, CitationHeaderView, CitationModalView, AnnotationView, MarkdownView, MetadataTemplate, DataSourceTemplate, PublishDoiTemplate, VersionTemplate, LoadingTemplate, ControlsTemplate, MetadataInfoIconsTemplate, AlertTemplate, EditMetadataTemplate, DataDisplayTemplate, MapTemplate, AnnotationTemplate, metaTagsHighwirePressTemplate, uuid, MetricView) { 'use strict'; /** * @class MetadataView * @classdesc A human-readable view of a science metadata file * @classcategory Views * @extends Backbone.View * @constructor * @screenshot views/MetadataView.png */ var MetadataView = Backbone.View.extend( /** @lends MetadataView.prototype */{ subviews: [], pid: null, seriesId: null, saveProvPending: false, model: new SolrResult(), packageModels: new Array(), dataPackage: null, el: '#Content', metadataContainer: "#metadata-container", citationContainer: "#citation-container", tableContainer: "#table-container", controlsContainer: "#metadata-controls-container", metricsContainer: "#metrics-controls-container", editorControlsContainer: "#editor-controls-container", breadcrumbContainer: "#breadcrumb-container", parentLinkContainer: "#parent-link-container", dataSourceContainer: "#data-source-container", articleContainer: "#article-container", type: "Metadata", //Templates template: _.template(MetadataTemplate), alertTemplate: _.template(AlertTemplate), doiTemplate: _.template(PublishDoiTemplate), versionTemplate: _.template(VersionTemplate), loadingTemplate: _.template(LoadingTemplate), controlsTemplate: _.template(ControlsTemplate), infoIconsTemplate: _.template(MetadataInfoIconsTemplate), dataSourceTemplate: _.template(DataSourceTemplate), editMetadataTemplate: _.template(EditMetadataTemplate), dataDisplayTemplate: _.template(DataDisplayTemplate), mapTemplate: _.template(MapTemplate), metaTagsHighwirePressTemplate: _.template(metaTagsHighwirePressTemplate), objectIds: [], // Delegated events for creating new items, and clearing completed ones. events: { "click #publish": "publish", "mouseover .highlight-node": "highlightNode", "mouseout .highlight-node": "highlightNode", "click .preview": "previewData", "click #save-metadata-prov": "saveProv" }, initialize: function (options) { if ((options === undefined) || (!options)) var options = {}; this.pid = options.pid || options.id || MetacatUI.appModel.get("pid") || null; if (typeof options.el !== "undefined") this.setElement(options.el); }, // Render the main metadata view render: function () { this.stopListening(); MetacatUI.appModel.set('headerType', 'default'); // this.showLoading("Loading..."); //Reset various properties of this view first this.classMap = new Array(); this.subviews = new Array(); this.model.set(this.model.defaults); this.packageModels = new Array(); // get the pid to render if (!this.pid) this.pid = MetacatUI.appModel.get("pid"); this.listenTo(MetacatUI.appUserModel, "change:loggedIn", this.render); //Listen to when the metadata has been rendered this.once("metadataLoaded", function () { this.createAnnotationViews(); this.insertMarkdownViews(); }); //Listen to when the package table has been rendered this.once("packageTableRendered", function () { //Scroll to the element on the page that is in the hash fragment (if there is one) this.scrollToFragment(); }); this.getModel(); return this; }, /** * Retrieve the resource map given its PID, and when it's fetched, * check for write permissions, then check for private members in the package * table view, if there is one. * @param {string} pid - The PID of the resource map */ getDataPackage: function (pid) { //Create a DataONEObject model to use in the DataPackage collection. var dataOneObject = new ScienceMetadata({ id: this.model.get("id") }); // Create a new data package with this id this.dataPackage = new DataPackage([dataOneObject], { id: pid }); this.dataPackage.mergeModels([this.model]); // If there is no resource map if (!pid) { this.checkWritePermissions(); return } this.listenToOnce(this.dataPackage, "complete", function () { var packageTableView = _.findWhere(this.subviews, { type: "PackageTable" }); if (packageTableView) { packageTableView.dataPackageCollection = this.dataPackage; packageTableView.checkForPrivateMembers(); } }); if (this.dataPackage.packageModel && this.dataPackage.packageModel.get("synced") === true) { this.checkWritePermissions(); } else { this.listenToOnce(this.dataPackage.packageModel, "sync", function () { this.checkWritePermissions(); }); } // Fetch the data package. DataPackage.parse() triggers 'complete' this.dataPackage.fetch({ fetchModels: false }); }, /* * Retrieves information from the index about this object, given the id (passed from the URL) * When the object info is retrieved from the index, we set up models depending on the type of object this is */ getModel: function (pid) { //Get the pid and sid if ((typeof pid === "undefined") || !pid) var pid = this.pid; if ((typeof this.seriesId !== "undefined") && this.seriesId) var sid = this.seriesId; //Get the package ID this.model.set({ id: pid, seriesId: sid }); var model = this.model; this.listenToOnce(model, "sync", function () { if (this.model.get("formatType") == "METADATA" || !this.model.get("formatType")) { this.model = model; this.renderMetadata(); } else if (this.model.get("formatType") == "DATA") { //Get the metadata pids that document this data object var isDocBy = this.model.get("isDocumentedBy"); //If there is only one metadata pid that documents this data object, then // get that metadata model for this view. if (isDocBy && isDocBy.length == 1) { this.navigateWithFragment(_.first(isDocBy), this.pid); return; } //If more than one metadata doc documents this data object, it is most likely // multiple versions of the same metadata. So we need to find the latest version. else if (isDocBy && isDocBy.length > 1) { var view = this; require(["collections/Filters", "collections/SolrResults"], function (Filters, SolrResults) { //Create a search for the metadata docs that document this data object var searchFilters = new Filters([{ values: isDocBy, fields: ["id", "seriesId"], operator: "OR", fieldsOperator: "OR", matchSubstring: false }]), //Create a list of search results searchResults = new SolrResults([], { rows: isDocBy.length, query: searchFilters.getQuery(), fields: "obsoletes,obsoletedBy,id" }); //When the search results are returned, process those results view.listenToOnce(searchResults, "sync", function (searchResults) { //Keep track of the latest version of the metadata doc(s) var latestVersions = []; //Iterate over each search result and find the latest version of each metadata version chain searchResults.each(function (searchResult) { //If this metadata isn't obsoleted by another object, it is the latest version if (!searchResult.get("obsoletedBy")) { latestVersions.push(searchResult.get("id")); } //If it is obsoleted by another object but that newer object does not document this data, then this is the latest version else if (!_.contains(isDocBy, searchResult.get("obsoletedBy"))) { latestVersions.push(searchResult.get("id")); } }, view); //If at least one latest version was found (should always be the case), if (latestVersions.length) { //Set that metadata pid as this view's pid and get that metadata model. // TODO: Support navigation to multiple metadata docs. This should be a rare occurence, but // it is possible that more than one metadata version chain documents a data object, and we need // to show the user that the data is involved in multiple datasets. view.navigateWithFragment(latestVersions[0], view.pid); } //If a latest version wasn't found, which should never happen, but just in case, default to the // last metadata pid in the isDocumentedBy field (most liekly to be the most recent since it was indexed last). else { view.navigateWithFragment(_.last(isDocBy), view.pid) } }); //Send the query to the Solr search service searchResults.query(); }); return; } else { this.noMetadata(this.model); } } else if (this.model.get("formatType") == "RESOURCE") { var packageModel = new Package({ id: this.model.get("id") }); packageModel.on("complete", function () { var metadata = packageModel.getMetadata(); if (!metadata) { this.noMetadata(packageModel); } else { this.model = metadata; this.pid = this.model.get("id"); this.renderMetadata(); if (this.model.get("resourceMap")) this.getPackageDetails(this.model.get("resourceMap")); } }, this); packageModel.getMembers(); return; } //Get the package information this.getPackageDetails(model.get("resourceMap")); }); //Listen to 404 and 401 errors when we get the metadata object this.listenToOnce(model, "404", this.showNotFound); this.listenToOnce(model, "401", this.showIsPrivate); //Fetch the model model.getInfo(); }, renderMetadata: function () { var pid = this.model.get("id"); this.hideLoading(); //Load the template which holds the basic structure of the view this.$el.html(this.template()); this.$(this.tableContainer).html(this.loadingTemplate({ msg: "Retrieving data set details..." })); //Insert the breadcrumbs this.insertBreadcrumbs(); //Insert the citation this.insertCitation(); //Insert the data source logo this.insertDataSource(); // is this the latest version? (includes DOI link when needed) this.showLatestVersion(); // Insert various metadata controls in the page this.insertControls(); // If we're displaying the metrics well then display copy citation and edit button // inside the well if (MetacatUI.appModel.get("displayDatasetMetrics")) { //Insert Metrics Stats into the dataset landing pages this.insertMetricsControls(); } //Show loading icon in metadata section this.$(this.metadataContainer).html(this.loadingTemplate({ msg: "Retrieving metadata ..." })); // Check for a view service in this MetacatUI.appModel if ((MetacatUI.appModel.get('viewServiceUrl') !== undefined) && (MetacatUI.appModel.get('viewServiceUrl'))) var endpoint = MetacatUI.appModel.get('viewServiceUrl') + encodeURIComponent(pid); if (endpoint && (typeof endpoint !== "undefined")) { var viewRef = this; var loadSettings = { url: endpoint, success: function (response, status, xhr) { //If the user has navigated away from the MetadataView, then don't render anything further if (MetacatUI.appView.currentView != viewRef) return; //Our fallback is to show the metadata details from the Solr index if (status == "error") viewRef.renderMetadataFromIndex(); else { //Check for a response that is a 200 OK status, but is an error msg if ((response.length < 250) && (response.indexOf("Error transforming document") > -1) && viewRef.model.get("indexed")) { viewRef.renderMetadataFromIndex(); return; } //Mark this as a metadata doc with no stylesheet, or one that is at least different than usual EML and FGDC else if ((response.indexOf('id="Metadata"') == -1)) { viewRef.$el.addClass("container no-stylesheet"); if (viewRef.model.get("indexed")) { viewRef.renderMetadataFromIndex(); return; } } //Now show the response from the view service viewRef.$(viewRef.metadataContainer).html(response); //If there is no info from the index and there is no metadata doc rendered either, then display a message if (viewRef.$el.is(".no-stylesheet") && viewRef.model.get("archived") && !viewRef.model.get("indexed")) viewRef.$(viewRef.metadataContainer).prepend(viewRef.alertTemplate({ msg: "There is limited metadata about this dataset since it has been archived." })); viewRef.alterMarkup(); viewRef.trigger("metadataLoaded"); //Add a map of the spatial coverage if (gmaps) viewRef.insertSpatialCoverageMap(); // Injects Clipboard objects into DOM elements returned from the View Service viewRef.insertCopiables(); } }, error: function (xhr, textStatus, errorThrown) { viewRef.renderMetadataFromIndex(); } } $.ajax(_.extend(loadSettings, MetacatUI.appUserModel.createAjaxSettings())); } else this.renderMetadataFromIndex(); // Insert the Linked Data into the header of the page. if (MetacatUI.appModel.get("isJSONLDEnabled")) { var json = this.generateJSONLD(); this.insertJSONLD(json); } this.insertCitationMetaTags(); }, /* If there is no view service available, then display the metadata fields from the index */ renderMetadataFromIndex: function () { var metadataFromIndex = new MetadataIndex({ pid: this.pid, parentView: this }); this.subviews.push(metadataFromIndex); //Add the metadata HTML this.$(this.metadataContainer).html(metadataFromIndex.render().el); var view = this; this.listenTo(metadataFromIndex, "complete", function () { //Add the package contents view.insertPackageDetails(); //Add a map of the spatial coverage if (gmaps) view.insertSpatialCoverageMap(); }); }, removeCitation: function () { var citation = "", citationEl = null; //Find the citation element if (this.$(".citation").length > 0) { //Get the text for the citation citation = this.$(".citation").text(); //Save this element in the view citationEl = this.$(".citation"); } //Older versions of Metacat (v2.4.3 and older) will not have the citation class in the XSLT. Find the citation another way else { //Find the DOM element with the citation var wells = this.$('.well'), viewRef = this; //Find the div.well with the citation. If we never find it, we don't insert the list of contents _.each(wells, function (well) { if (!citationEl && ($(well).find('#viewMetadataCitationLink').length > 0) || ($(well).children(".row-fluid > .span10 > a"))) { //Save this element in the view citationEl = well; //Mark this in the DOM for CSS styling $(well).addClass('citation'); //Save the text of the citation citation = $(well).text(); } }); //Remove the unnecessary classes that are used in older versions of Metacat (2.4.3 and older) var citationText = $(citationEl).find(".span10"); $(citationText).removeClass("span10").addClass("span12"); } //Set the document title to the citation MetacatUI.appModel.set("title", citation); citationEl.remove(); }, insertBreadcrumbs: function () { var breadcrumbs = $(document.createElement("ol")) .addClass("breadcrumb") .append($(document.createElement("li")) .addClass("home") .append($(document.createElement("a")) .attr("href", MetacatUI.root || "/") .addClass("home") .text("Home"))) .append($(document.createElement("li")) .addClass("search") .append($(document.createElement("a")) .attr("href", MetacatUI.root + "/data" + ((MetacatUI.appModel.get("page") > 0) ? ("/page/" + (parseInt(MetacatUI.appModel.get("page")) + 1)) : "")) .addClass("search") .text("Search"))) .append($(document.createElement("li")) .append($(document.createElement("a")) .attr("href", MetacatUI.root + "/view/" + encodeURIComponent(this.pid)) .addClass("inactive") .text("Metadata"))); if (MetacatUI.uiRouter.lastRoute() == "data") { $(breadcrumbs).prepend($(document.createElement("a")) .attr("href", MetacatUI.root + "/data/page/" + ((MetacatUI.appModel.get("page") > 0) ? (parseInt(MetacatUI.appModel.get("page")) + 1) : "")) .attr("title", "Back") .addClass("back") .text(" Back to search") .prepend($(document.createElement("i")) .addClass("icon-angle-left"))); $(breadcrumbs).find("a.search").addClass("inactive"); } this.$(this.breadcrumbContainer).html(breadcrumbs); }, /* * When the metadata object doesn't exist, display a message to the user */ showNotFound: function () { //If the model was found, exit this function if (!this.model.get("notFound")) { return; } try { //Check if a query string was in the URL and if so, try removing it in the identifier if (this.model.get("id").match(/\?\S+\=\S+/g) && !this.findTries) { let newID = this.model.get("id").replace(/\?\S+\=\S+/g, ""); this.onClose(); this.model.set("id", newID); this.pid = newID; this.findTries = 1; this.render(); return; } } catch (e) { console.warn("Caught error while determining query string", e); } //Construct a message that shows this object doesn't exist var msg = "

Nothing was found.

" + "

The dataset identifier '" + Utilities.encodeHTML(this.model.get("id")) + "' " + "does not exist or it may have been removed. Search for " + "datasets that mention " + Utilities.encodeHTML(this.model.get("id")) + "

"; //Remove the loading message this.hideLoading(); //Show the not found error message this.showError(msg); //Add the pid to the link href. Add via JS so it is Attribute-encoded to prevent XSS attacks this.$("#metadata-view-not-found-message a").attr("href", MetacatUI.root + "/data/query=" + encodeURIComponent(this.model.get("id"))); }, /* * When the metadata object is private, display a message to the user */ showIsPrivate: function () { //If we haven't checked the logged-in status of the user yet, wait a bit //until we show a 401 msg, in case this content is their private content if (!MetacatUI.appUserModel.get("checked")) { this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.showIsPrivate); return; } //If the user is logged in, the message will display that this dataset is private. if (MetacatUI.appUserModel.get("loggedIn")) { var msg = '' + '' + '' + ' This is a private dataset.'; } //If the user isn't logged in, display a log in link. else { var msg = '' + '' + '' + ' This is a private dataset. If you believe you have permission ' + 'to access this dataset, then sign in.'; } //Remove the loading message this.hideLoading(); //Show the not found error message this.showError(msg); }, getPackageDetails: function (packageIDs) { var completePackages = 0; //This isn't a package, but just a lonely metadata doc... if (!packageIDs || !packageIDs.length) { var thisPackage = new Package({ id: null, members: [this.model] }); thisPackage.flagComplete(); this.packageModels = [thisPackage]; this.insertPackageDetails(thisPackage); } else { _.each(packageIDs, function (thisPackageID, i) { //Create a model representing the data package var thisPackage = new Package({ id: thisPackageID }); //Listen for any parent packages this.listenToOnce(thisPackage, "change:parentPackageMetadata", this.insertParentLink); //When the package info is fully retrieved this.listenToOnce(thisPackage, 'complete', function (thisPackage) { //When all packages are fully retrieved completePackages++; if (completePackages >= packageIDs.length) { var latestPackages = _.filter(this.packageModels, function (m) { return !_.contains(packageIDs, m.get("obsoletedBy")); }); //Set those packages as the most recent package this.packageModels = latestPackages; this.insertPackageDetails(latestPackages); } }); //Save the package in the view this.packageModels.push(thisPackage); //Make sure we get archived content, too thisPackage.set("getArchivedMembers", true); //Get the members thisPackage.getMembers({ getParentMetadata: true }); }, this); } }, alterMarkup: function () { //Find the taxonomic range and give it a class for styling - for older versions of Metacat only (v2.4.3 and older) if (!this.$(".taxonomicCoverage").length) this.$('h4:contains("Taxonomic Range")').parent().addClass('taxonomicCoverage'); //Remove ecogrid links and replace them with workable links this.replaceEcoGridLinks(); //Find the tab links for attribute names this.$(".attributeListTable tr a").on('shown', function (e) { //When the attribute link is clicked on, highlight the tab as active $(e.target).parents(".attributeListTable").find(".active").removeClass("active"); $(e.target).parents("tr").first().addClass("active"); }); //Mark the first row in each attribute list table as active since the first attribute is displayed at first this.$(".attributeListTable tr:first-child()").addClass("active"); }, /* * Inserts a table with all the data package member information and sends the call to display annotations */ insertPackageDetails: function (packages) { //Don't insert the package details twice var tableEls = this.$(this.tableContainer).children().not(".loading"); if (tableEls.length > 0) return; //wait for the metadata to load var metadataEls = this.$(this.metadataContainer).children(); if (!metadataEls.length || metadataEls.first().is(".loading")) { this.once("metadataLoaded", this.insertPackageDetails); return; } if (!packages) var packages = this.packageModels; //Get the entity names from this page/metadata this.getEntityNames(packages); _.each(packages, function (packageModel) { //If the package model is not complete, don't do anything if (!packageModel.complete) return; //Insert a package table for each package in viewRef dataset var nestedPckgs = packageModel.getNestedPackages(), nestedPckgsToDisplay = []; //If this metadata is not archived, filter out archived packages if (!this.model.get("archived")) { nestedPckgsToDisplay = _.reject(nestedPckgs, function (pkg) { return (pkg.get("archived")) }); } else { //Display all packages is this metadata is archived nestedPckgsToDisplay = nestedPckgs; } if (nestedPckgsToDisplay.length > 0) { if (!(!this.model.get("archived") && packageModel.get("archived") == true)) { var title = 'Current Data Set (1 of ' + (nestedPckgsToDisplay.length + 1) + ') Package: ' + packageModel.get("id") + ''; 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) + ') Package: ' + nestedPackage.get("id") + ' (View ) '; 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") ? 'Package: ' + packageModel.get("id") + '' : ""; title = "Files in this dataset " + title; this.insertPackageTable(packageModel, { title: title }); } } //Remove the extra download button returned from the XSLT since the package table will have all the download links $("#downloadPackage").remove(); }, this); //Collapse the table list after the first table var additionalTables = $(this.$("#additional-tables-for-" + this.cid)), numTables = additionalTables.children(".download-contents").length, item = (numTables == 1) ? "dataset" : "datasets"; if (numTables > 0) { var expandIcon = $(document.createElement("i")).addClass("icon icon-level-down"), expandLink = $(document.createElement("a")) .attr("href", "#") .addClass("toggle-slide toggle-display-on-slide") .attr("data-slide-el", "additional-tables-for-" + this.cid) .text("Show " + numTables + " nested " + item) .prepend(expandIcon), collapseLink = $(document.createElement("a")) .attr("href", "#") .addClass("toggle-slide toggle-display-on-slide") .attr("data-slide-el", "additional-tables-for-" + this.cid) .text("Hide nested " + item) .hide(), expandControl = $(document.createElement("div")).addClass("expand-collapse-control").append(expandLink, collapseLink); additionalTables.before(expandControl); } //If this metadata doc is not in a package, but is just a lonely metadata doc... if (!packages.length) { var packageModel = new Package({ members: [this.model], }); packageModel.complete = true; this.insertPackageTable(packageModel); } //Insert the data details sections this.insertDataDetails(); // Get data package, if there is one, before checking write permissions if (packages.length) { this.getDataPackage(packages[0].get("id")); } else { // Otherwise go ahead and check write permissions on metadata only this.checkWritePermissions(); } try { // Get the most recent package to display the provenance graphs if (packages.length) { //Find the most recent Package model and fetch it let mostRecentPackage = _.find(packages, p => !p.get("obsoletedBy")); //If all of the packages are obsoleted, then use the last package in the array, // which is most likely the most recent. /** @todo Use the DataONE version API to find the most recent package in the version chain */ if (!mostRecentPackage) { mostRecentPackage = packages[packages.length - 1]; } //Get the data package this.getDataPackage(mostRecentPackage.get("id")); } } catch (e) { console.error("Could not get the data package (prov will not be displayed, possibly other info as well).", e); } //Initialize tooltips in the package table(s) this.$(".tooltip-this").tooltip(); return this; }, insertPackageTable: function (packageModel, options) { var viewRef = this; if (options) { var title = options.title || ""; var nested = (typeof options.nested === "undefined") ? false : options.nested; } else var title = "", nested = false; if (typeof packageModel === "undefined") return; //** Draw the package table **// var tableView = new PackageTable({ model: packageModel, currentlyViewing: this.pid, parentView: this, title: title, nested: nested, metricsModel: this.metricsModel }); //Get the package table container var tablesContainer = this.$(this.tableContainer); //After the first table, start collapsing them var numTables = $(tablesContainer).find("table.download-contents").length; if (numTables == 1) { var tableContainer = $(document.createElement("div")).attr("id", "additional-tables-for-" + this.cid); tableContainer.hide(); $(tablesContainer).append(tableContainer); } else if (numTables > 1) var tableContainer = this.$("#additional-tables-for-" + this.cid); else var tableContainer = tablesContainer; //Insert the package table HTML $(tableContainer).append(tableView.render().el); $(this.tableContainer).children(".loading").remove(); $(tableContainer).find(".tooltip-this").tooltip(); this.subviews.push(tableView); //Trigger a custom event in this view that indicates the package table has been rendered this.trigger("packageTableRendered"); }, insertParentLink: function (packageModel) { var parentPackageMetadata = packageModel.get("parentPackageMetadata"), view = this; _.each(parentPackageMetadata, function (m, i) { var title = m.get("title"), icon = $(document.createElement("i")).addClass("icon icon-on-left icon-level-up"), link = $(document.createElement("a")).attr("href", MetacatUI.root + "/view/" + encodeURIComponent(m.get("id"))) .addClass("parent-link") .text("Parent dataset: " + title) .prepend(icon); view.$(view.parentLinkContainer).append(link); }); }, insertSpatialCoverageMap: function (customCoordinates) { //Find the geographic region container. Older versions of Metacat (v2.4.3 and less) will not have it classified so look for the header text if (!this.$(".geographicCoverage").length) { //For EML var title = this.$('h4:contains("Geographic Region")'); //For FGDC if (title.length == 0) { title = this.$('label:contains("Bounding Coordinates")'); } var georegionEls = $(title).parent(); var parseText = true; var directions = new Array('North', 'South', 'East', 'West'); } else { var georegionEls = this.$(".geographicCoverage"); var directions = new Array('north', 'south', 'east', 'west'); } for (var i = 0; i < georegionEls.length; i++) { var georegion = georegionEls[i]; if (typeof customCoordinates !== "undefined") { //Extract the coordinates var n = customCoordinates[0]; var s = customCoordinates[1]; var e = customCoordinates[2]; var w = customCoordinates[3]; } else { var coordinates = new Array(); _.each(directions, function (direction) { //Parse text for older versions of Metacat (v2.4.3 and earlier) if (parseText) { var labelEl = $(georegion).find('label:contains("' + direction + '")'); if (labelEl.length) { var coordinate = $(labelEl).next().html(); if (typeof coordinate != "undefined" && coordinate.indexOf(" ") > -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(); //If there isn't a center point found, don't draw the map. if (typeof latLngCEN == "undefined") { return; } var url = "https://maps.google.com/?ll=" + latLngCEN.lat() + "," + latLngCEN.lng() + "&spn=0.003833,0.010568" + "&t=m" + "&z=5"; //Get the map path color var pathColor = MetacatUI.appModel.get("datasetMapPathColor"); if (pathColor) { pathColor = "color:" + pathColor + "|"; } else { pathColor = ""; } //Get the map path fill color var fillColor = MetacatUI.appModel.get("datasetMapFillColor"); if (fillColor) { fillColor = "fillcolor:" + fillColor + "|"; } else { fillColor = ""; } //Create a google map image var mapHTML = ""; //Find the spot in the DOM to insert our map image if (parseText) var insertAfter = ($(georegion).find('label:contains("West")').parent().parent().length) ? $(georegion).find('label:contains("West")').parent().parent() : georegion; //The last coordinate listed else var insertAfter = georegion; $(insertAfter).append(this.mapTemplate({ map: mapHTML, url: url })); $('.fancybox-media').fancybox({ openEffect: 'elastic', closeEffect: 'elastic', helpers: { media: {} } }) } return true; }, insertCitation: function () { if (!this.model) return false; //Create a citation header element from the model attributes var header = new CitationHeaderView({ model: this.model }); this.$(this.citationContainer).html(header.render().el); }, insertDataSource: function () { if (!this.model || !MetacatUI.nodeModel || !MetacatUI.nodeModel.get("members").length || !this.$(this.dataSourceContainer).length) return; var dataSource = MetacatUI.nodeModel.getMember(this.model), replicaMNs = MetacatUI.nodeModel.getMembers(this.model.get("replicaMN")); //Filter out the data source from the replica nodes if (Array.isArray(replicaMNs) && replicaMNs.length) { replicaMNs = _.without(replicaMNs, dataSource); } if (dataSource && dataSource.logo) { this.$("img.data-source").remove(); //Construct a URL to the profile of this repository var profileURL = (dataSource.identifier == MetacatUI.appModel.get("nodeId")) ? MetacatUI.root + "/profile" : MetacatUI.appModel.get("dataoneSearchUrl") + "/portals/" + dataSource.shortIdentifier; //Insert the data source template this.$(this.dataSourceContainer).html(this.dataSourceTemplate({ node: dataSource, profileURL: profileURL })).addClass("has-data-source"); this.$(this.citationContainer).addClass("has-data-source"); this.$(".tooltip-this").tooltip(); $(".popover-this.data-source.logo").popover({ trigger: "manual", html: true, title: "From the " + dataSource.name + " repository", content: function () { var content = "

" + dataSource.description + "

"; if (replicaMNs.length) { content += '
Exact copies hosted by ' + replicaMNs.length + ' repositories:
"; } return content; }, animation: false }) .on("mouseenter", function () { var _this = this; $(this).popover("show"); $(".popover").on("mouseleave", function () { $(_this).popover('hide'); }); }).on("mouseleave", function () { var _this = this; setTimeout(function () { if (!$(".popover:hover").length) { $(_this).popover("hide"); } }, 300); }); } }, /** * Check whether the user has write permissions on the resource map and the EML. * Once the permission checks have finished, continue with the functions that * depend on them. */ checkWritePermissions: function () { var view = this, authorization = [], resourceMap = this.dataPackage ? this.dataPackage.packageModel : null, modelsToCheck = [this.model, resourceMap]; modelsToCheck.forEach(function (model, index) { // If there is no resource map or no EML, // then the user does not need permission to edit it. if (!model || model.get("notFound") == true) { authorization[index] = true // If we already checked, and the user is authorized, // record that information in the authorzation array. } else if (model.get("isAuthorized_write") === true) { authorization[index] = true // If we already checked, and the user is not authorized, // record that information in the authorzation array. } else if (model.get("isAuthorized_write") === false) { authorization[index] = false // If we haven't checked for authorization yet, do that now. // Return to this function once we've finished checking. } else { view.stopListening(model, "change:isAuthorized_write"); view.listenToOnce(model, "change:isAuthorized_write", function () { view.checkWritePermissions(); }); view.stopListening(model, "change:notFound"); view.listenToOnce(model, "change:notFound", function () { view.checkWritePermissions(); }); model.checkAuthority("write"); return } }); // Check that all the models were tested for authorization // Every value in the auth array must be true for the user to have full permissions var allTrue = _.every(authorization, function (test) { return test }), // When we have completed checking each of the models that we need to check for // permissions, every value in the authorization array should be "true" or "false", // and the array should have the same length as the modelsToCheck array. allBoolean = _.every(authorization, function (test) { return typeof test === "boolean" }), allChecked = allBoolean && authorization.length === modelsToCheck.length; // Check for and render prov diagrams now that we know whether or not the user has editor permissions // (There is a different version of the chart for users who can edit the resource map and users who cannot) if (allChecked) { this.checkForProv(); } else { return } // Only render the editor controls if we have completed the checks AND the user has full editor permissions if (allTrue) { this.insertEditorControls(); } }, /* * Inserts control elements onto the page for the user to interact with the dataset - edit, publish, etc. * Editor permissions should already have been checked before running this function. */ insertEditorControls: function () { var view = this, resourceMap = this.dataPackage ? this.dataPackage.packageModel : null, modelsToCheck = [this.model, resourceMap], authorized = _.every(modelsToCheck, function (model) { // If there is no EML or no resource map, the user doesn't need permission to edit it. return (!model || model.get("notFound") == true) ? true : model.get("isAuthorized_write") === true; }); // Only run this function when the user has full editor permissions // (i.e. write permission on the EML, and write permission on the resource map if there is one.) if (!authorized) { return } if ( (this.model.get("obsoletedBy") && (this.model.get("obsoletedBy").length > 0)) || this.model.get("archived") ) { return false; } // Save the element that will contain the owner control buttons var container = this.$(this.editorControlsContainer); // Do not insert the editor controls twice container.empty(); // The PID for the EML model var pid = this.model.get("id") || this.pid; //Insert an Edit button if the Edit button is enabled if (MetacatUI.appModel.get("displayDatasetEditButton")) { //Check that this is an editable metadata format if (_.contains(MetacatUI.appModel.get("editableFormats"), this.model.get("formatId"))) { //Insert the Edit Metadata template container.append( this.editMetadataTemplate({ identifier: pid, supported: true })); } //If this format is not editable, insert an unspported Edit Metadata template else { container.append(this.editMetadataTemplate({ supported: false })); } } try { //Determine if this metadata can be published. // The Publish feature has to be enabled in the app. // The model cannot already have a DOI var canBePublished = MetacatUI.appModel.get("enablePublishDOI") && !view.model.isDOI(); //If publishing is enabled, check if only certain users and groups can publish metadata if (canBePublished) { //Get the list of authorized publishers from the AppModel var authorizedPublishers = MetacatUI.appModel.get("enablePublishDOIForSubjects"); //If the logged-in user is one of the subjects in the list or is in a group that is // in the list, then this metadata can be published. Otherwise, it cannot. if (Array.isArray(authorizedPublishers) && authorizedPublishers.length) { if (MetacatUI.appUserModel.hasIdentityOverlap(authorizedPublishers)) { canBePublished = true; } else { canBePublished = false; } } } //If this metadata can be published, then insert the Publish button template if (canBePublished) { //Insert a Publish button template container.append( view.doiTemplate({ isAuthorized: true, identifier: pid })); } } catch (e) { console.error("Cannot display the publish button: ", e); } }, /* * Injects Clipboard objects onto DOM elements returned from the Metacat * View Service. This code depends on the implementation of the Metacat * View Service in that it depends on elements with the class "copy" being * contained in the HTML returned from the View Service. * * To add more copiable buttons (or other elements) to a View Service XSLT, * you should be able to just add something like: * * * * to your XSLT and this should pick it up automatically. */ insertCopiables: function () { var copiables = $("#Metadata .copy"); _.each(copiables, function (copiable) { var clipboard = new Clipboard(copiable); clipboard.on("success", function (e) { var el = $(e.trigger); $(el).html($(document.createElement("span")).addClass("icon icon-ok success")); // Use setTimeout instead of jQuery's built-in Events system because // it didn't look flexible enough to allow me update innerHTML in // a chain setTimeout(function () { $(el).html("Copy"); }, 500) }); }); }, /* * Inserts elements users can use to interact with this dataset: * - A "Copy Citation" button to copy the citation text */ insertControls: function () { // Convert the support mdq formatId list to a version // that JS regex likes (with special characters double RegExp.escape = function (s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\\\$&'); }; var mdqFormatIds = MetacatUI.appModel.get("mdqFormatIds"); // Check of the current formatId is supported by the current // metadata quality suite. If not, the 'Assessment Report' button // will not be displacyed in the metadata controls panel. var thisFormatId = this.model.get("formatId"); var mdqFormatSupported = false; var formatFound = false; if (mdqFormatIds !== null) { for (var ifmt = 0; ifmt < mdqFormatIds.length; ++ifmt) { var currentFormatId = RegExp.escape(mdqFormatIds[ifmt]); var re = new RegExp(currentFormatId); formatFound = re.test(thisFormatId); if (formatFound) { break; } } } //Get template var controlsContainer = this.controlsTemplate({ citationTarget: this.citationContainer, url: window.location, displayQualtyReport: MetacatUI.appModel.get("mdqBaseUrl") && formatFound && MetacatUI.appModel.get("displayDatasetQualityMetric"), showWholetale: MetacatUI.appModel.get("showWholeTaleFeatures"), model: this.model.toJSON() }); $(this.controlsContainer).html(controlsContainer); //Insert the info icons var metricsWell = this.$(".metrics-container"); metricsWell.append(this.infoIconsTemplate({ model: this.model.toJSON() })); if (MetacatUI.appModel.get("showWholeTaleFeatures")) { this.createWholeTaleButton(); } // Show the citation modal with the ability to copy the citation text // when the "Copy Citation" button is clicked const citeButton = this.el.querySelector('#cite-this-dataset-btn'); if (citeButton) { citeButton.removeEventListener('click', this.citationModal); citeButton.addEventListener('click', () => { this.citationModal = new CitationModalView({ model: this.model, createLink: true }) this.subviews.push(this.citationModal); this.citationModal.render(); }, false); } }, /** *Creates a button which the user can click to launch the package in Whole Tale */ createWholeTaleButton: function () { let self = this; MetacatUI.appModel.get('taleEnvironments').forEach(function (environment) { var queryParams = '?uri=' + window.location.href + '&title=' + encodeURIComponent(self.model.get("title")) + '&environment=' + environment + '&api=' + MetacatUI.appModel.get("d1CNBaseUrl") + MetacatUI.appModel.get("d1CNService"); var composeUrl = MetacatUI.appModel.get('dashboardUrl') + queryParams; var anchor = $(''); anchor.attr('href', composeUrl).append( $('').attr('class', 'tab').append(environment)); anchor.attr('target', '_blank'); $('.analyze.dropdown-menu').append($('
  • ').append(anchor)); }); }, // Inserting the Metric Stats insertMetricsControls: function () { //Exit if metrics shouldn't be shown for this dataset if (this.model.hideMetrics()) { return; } var pid_list = []; pid_list.push(this.pid); var metricsModel = new MetricsModel({ pid_list: pid_list, type: "dataset" }); metricsModel.fetch(); this.metricsModel = metricsModel; // Retreive the model from the server for the given PID // TODO: Create a Metric Request Object if (MetacatUI.appModel.get("displayDatasetMetrics")) { var buttonToolbar = this.$(".metrics-container"); if (MetacatUI.appModel.get("displayDatasetDownloadMetric")) { var dwnldsMetricView = new MetricView({ metricName: 'Downloads', model: metricsModel, pid: this.pid }); buttonToolbar.append(dwnldsMetricView.render().el); this.subviews.push(dwnldsMetricView); } if (MetacatUI.appModel.get("displayDatasetCitationMetric")) { var citationsMetricView = new MetricView({ metricName: 'Citations', model: metricsModel, pid: this.pid }); buttonToolbar.append(citationsMetricView.render().el); this.subviews.push(citationsMetricView); try { //Check if the registerCitation=true query string is set if (window.location.search) { if (window.location.search.indexOf("registerCitation=true") > -1) { //Open the modal for the citations citationsMetricView.showMetricModal(); //Show the register citation form if (citationsMetricView.modalView) { citationsMetricView.modalView.on("renderComplete", citationsMetricView.modalView.showCitationForm); } } } } catch (e) { console.warn("Not able to show the register citation form ", e); } } if (MetacatUI.appModel.get("displayDatasetViewMetric")) { var viewsMetricView = new MetricView({ metricName: 'Views', model: metricsModel, pid: this.pid }); buttonToolbar.append(viewsMetricView.render().el); this.subviews.push(viewsMetricView); } } }, /** * Check if the DataPackage provenance parsing has completed. If it has, * draw provenance charts. If it hasn't start the parseProv function. * The view must have the DataPackage collection set as view.dataPackage * for this function to run. */ checkForProv: function () { if (!this.dataPackage) { return } // Render the provenance trace using the redrawProvCharts function instead of the drawProvCharts function // just in case the prov charts have already been inserted. Redraw will make sure they are removed // before being re-inserted. var model = this.model; if (this.dataPackage.provenanceFlag == "complete") { this.redrawProvCharts(this.dataPackage); } else { this.listenToOnce(this.dataPackage, "queryComplete", function () { this.redrawProvCharts(this.dataPackage); }); // parseProv triggers "queryComplete" this.dataPackage.parseProv(); } }, /* * Renders ProvChartViews on the page to display provenance on a package level and on an individual object level. * This function looks at four sources for the provenance - the package sources, the package derivations, member sources, and member derivations */ drawProvCharts: function (dataPackage) { // Set a listener to re-draw the prov charts when needed this.stopListening(this.dataPackage, "redrawProvCharts"); this.listenToOnce(this.dataPackage, "redrawProvCharts", this.redrawProvCharts); // Provenance has to be retrieved from the Package Model (getProvTrace()) before the charts can be drawn if (dataPackage.provenanceFlag != "complete") return false; // If the user is authorized to edit the provenance for this package // then turn on editing, so that edit icons are displayed. var editModeOn = this.dataPackage.packageModel.get("isAuthorized_write"); //If this content is archived, then turn edit mode off if (this.model.get("archived")) { editModeOn = false; } //If none of the models in this package have the formatId attributes, // we should fetch the DataPackage since it likely has only had a shallow fetch so far var formats = _.compact(dataPackage.pluck("formatId")); //If the number of formatIds is less than the number of models in this collection, // then we need to get them. if (formats.length < dataPackage.length) { var modelsToMerge = []; //Get the PackageModel associated with this view if (this.packageModels.length) { //Get the PackageModel for this DataPackage var packageModel = _.find(this.packageModels, function (packageModel) { return packageModel.get("id") == dataPackage.id }); //Merge the SolrResult models into the DataONEObject models if (packageModel && packageModel.get("members").length) { modelsToMerge = packageModel.get("members"); } } //If there is at least one model to merge into this data package, do so if (modelsToMerge.length) { dataPackage.mergeModels(modelsToMerge); } //If there are no models to merge in, get them from the index else { //Listen to the DataPackage fetch to complete and re-execute this function this.listenToOnce(dataPackage, "complete", function () { this.drawProvCharts(dataPackage); }); //Create a query that searches for all the members of this DataPackage in Solr dataPackage.solrResults.currentquery = dataPackage.filterModel.getQuery() + "%20AND%20-formatType:METADATA"; dataPackage.solrResults.fields = "id,seriesId,formatId,fileName"; dataPackage.solrResults.rows = dataPackage.length; dataPackage.solrResults.sort = null; dataPackage.solrResults.start = 0; dataPackage.solrResults.facet = []; dataPackage.solrResults.stats = null; //Fetch the data package with the "fromIndex" option dataPackage.fetch({ fromIndex: true }); //Exit this function since it will be executed again when the fetch is complete return; } } var view = this; //Draw two flow charts to represent the sources and derivations at a package level var packageSources = dataPackage.sourcePackages; var packageDerivations = dataPackage.derivationPackages; if (Object.keys(packageSources).length) { var sourceProvChart = new ProvChart({ sources: packageSources, context: dataPackage, contextEl: this.$(this.articleContainer), dataPackage: dataPackage, parentView: view }); this.subviews.push(sourceProvChart); this.$(this.articleContainer).before(sourceProvChart.render().el); } if (Object.keys(packageDerivations).length) { var derivationProvChart = new ProvChart({ derivations: packageDerivations, context: dataPackage, contextEl: this.$(this.articleContainer), dataPackage: dataPackage, parentView: view }); this.subviews.push(derivationProvChart); this.$(this.articleContainer).after(derivationProvChart.render().el); } if (dataPackage.sources.length || dataPackage.derivations.length || editModeOn) { //Draw the provenance charts for each member of this package at an object level _.each(dataPackage.toArray(), function (member, i) { // Don't draw prov charts for metadata objects. if (member.get("type").toLowerCase() == "metadata" || member.get("formatType").toLowerCase() == "metadata") { return; } var entityDetailsSection = view.findEntityDetailsContainer(member); if (!entityDetailsSection) { return; } //Retrieve the sources and derivations for this member var memberSources = member.get("provSources") || new Array(), memberDerivations = member.get("provDerivations") || new Array(); //Make the source chart for this member. // If edit is on, then either a 'blank' sources ProvChart will be displayed if there // are no sources for this member, or edit icons will be displayed with prov icons. if (memberSources.length || editModeOn) { var memberSourcesProvChart = new ProvChart({ sources: memberSources, context: member, contextEl: entityDetailsSection, dataPackage: dataPackage, parentView: view, editModeOn: editModeOn, editorType: "sources" }); view.subviews.push(memberSourcesProvChart); $(entityDetailsSection).before(memberSourcesProvChart.render().el); view.$(view.articleContainer).addClass("gutters"); } //Make the derivation chart for this member // If edit is on, then either a 'blank' derivations ProvChart will be displayed if there, // are no derivations for this member or edit icons will be displayed with prov icons. if (memberDerivations.length || editModeOn) { var memberDerivationsProvChart = new ProvChart({ derivations: memberDerivations, context: member, contextEl: entityDetailsSection, dataPackage: dataPackage, parentView: view, editModeOn: editModeOn, editorType: "derivations" }); view.subviews.push(memberDerivationsProvChart); $(entityDetailsSection).after(memberDerivationsProvChart.render().el); view.$(view.articleContainer).addClass("gutters"); } }); } //Make all of the prov chart nodes look different based on id if (this.$(".prov-chart").length > 10000) { var allNodes = this.$(".prov-chart .node"), ids = [], view = this, i = 1; $(allNodes).each(function () { ids.push($(this).attr("data-id")) }); ids = _.uniq(ids); _.each(ids, function (id) { var matchingNodes = view.$(".prov-chart .node[data-id='" + id + "']").not(".editorNode"); //var matchingEntityDetails = view.findEntityDetailsContainer(id); //Don't use the unique class on images since they will look a lot different anyway by their image if (!$(matchingNodes).first().hasClass("image")) { var className = "uniqueNode" + i; //Add the unique class and up the iterator if (matchingNodes.prop("tagName") != "polygon") $(matchingNodes).addClass(className); else $(matchingNodes).attr("class", $(matchingNodes).attr("class") + " " + className); /* if(matchingEntityDetails) $(matchingEntityDetails).addClass(className);*/ //Save this id->class mapping in this view view.classMap.push({ id: id, className: className }); i++; } }); } }, /* Step through all prov charts and re-render each one that has been marked for re-rendering. */ redrawProvCharts: function () { var view = this; // Check if prov edits are active and turn on the prov save bar if so. // Alternatively, turn off save bar if there are no prov edits, which // could occur if a user undoes a previous which could result in // an empty edit list. if (this.dataPackage.provEditsPending()) { this.showEditorControls(); } else { this.hideEditorControls(); // Reset the edited flag for each package member _.each(this.dataPackage.toArray(), function (item) { item.selectedInEditor == false; }); } _.each(this.subviews, function (thisView, i) { // Check if this is a ProvChartView if (thisView.className && thisView.className.indexOf("prov-chart") !== -1) { // Check if this ProvChartView is marked for re-rendering // Erase the current ProvChartView thisView.onClose(); } }); // Remove prov charts from the array of subviews. this.subviews = _.filter(this.subviews, function (item) { return (item.className && (item.className.indexOf("prov-chart") == -1)); }); view.drawProvCharts(this.dataPackage); }, /* * When the data package collection saves successfully, tell the user */ saveSuccess: function (savedObject) { //We only want to perform these actions after the package saves if (savedObject.type != "DataPackage") return; //Change the URL to the new id MetacatUI.uiRouter.navigate("view/" + this.dataPackage.packageModel.get("id"), { trigger: false, replace: true }); var message = $(document.createElement("div")).append($(document.createElement("span")).text("Your changes have been saved. ")); MetacatUI.appView.showAlert(message, "alert-success", "body", 4000, { remove: false }); // Reset the state to clean this.dataPackage.packageModel.set("changed", false); // If provenance relationships were updated, then reset the edit list now. if (this.dataPackage.provEdits.length) this.dataPackage.provEdits = []; this.saveProvPending = false; this.hideSaving(); this.stopListening(this.dataPackage, "errorSaving", this.saveError); // Turn off "save" footer this.hideEditorControls(); // Update the metadata table header with the new resource map id. // First find the PackageTableView for the top level package, and // then re-render it with the update resmap id. var view = this; var metadataId = this.packageModels[0].getMetadata().get("id") _.each(this.subviews, function (thisView, i) { // Check if this is a ProvChartView if (thisView.type && thisView.type.indexOf("PackageTable") !== -1) { if (thisView.currentlyViewing == metadataId) { var packageId = view.dataPackage.packageModel.get("id"); var title = packageId ? 'Package: ' + packageId + '' : ""; thisView.title = "Files in this dataset " + title; thisView.render(); } } }); }, /* * When the data package collection fails to save, tell the user */ saveError: function (errorMsg) { var errorId = "error" + Math.round(Math.random() * 100), message = $(document.createElement("div")).append("

    Your changes could not be saved.

    "); 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(' Saving...') .addClass("btn-disabled"); this.$("input, textarea, select, button").prop("disabled", true); }, hideSaving: function () { this.$("input, textarea, select, button").prop("disabled", false); //When prov is saved, revert the Save button back to normal this.$("#save-metadata-prov").html("Save").removeClass("btn-disabled"); }, showEditorControls: function () { this.$("#editor-footer").slideDown(); }, hideEditorControls: function () { this.$("#editor-footer").slideUp(); }, getEntityNames: function (packageModels) { var viewRef = this; _.each(packageModels, function (packageModel) { //Don't get entity names for larger packages - users must put the names in the system metadata if (packageModel.get("members").length > 100) return; //If this package has a different metadata doc than the one we are currently viewing var metadataModel = packageModel.getMetadata(); if (!metadataModel) return; if (metadataModel.get("id") != viewRef.pid) { var requestSettings = { url: MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(metadataModel.get("id")), success: function (parsedMetadata, response, xhr) { _.each(packageModel.get("members"), function (solrResult, i) { var entityName = ""; if (solrResult.get("formatType") == "METADATA") entityName = solrResult.get("title"); var container = viewRef.findEntityDetailsContainer(solrResult, parsedMetadata); if (container) entityName = viewRef.getEntityName(container); //Set the entity name if (entityName) { solrResult.set("fileName", entityName); //Update the UI with the new name viewRef.$(".entity-name-placeholder[data-id='" + solrResult.get("id") + "']").text(entityName); } }); } } $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); return; } _.each(packageModel.get("members"), function (solrResult, i) { var entityName = ""; if (solrResult.get("fileName")) entityName = solrResult.get("fileName"); else if (solrResult.get("formatType") == "METADATA") entityName = solrResult.get("title"); else if (solrResult.get("formatType") == "RESOURCE") return; else { var container = viewRef.findEntityDetailsContainer(solrResult); if (container && container.length > 0) entityName = viewRef.getEntityName(container); else entityName = null; } //Set the entityName, even if it's null solrResult.set("fileName", entityName); }); }); }, getEntityName: function (containerEl) { if (!containerEl) return false; var entityName = $(containerEl).find(".entityName").attr("data-entity-name"); if ((typeof entityName === "undefined") || (!entityName)) { entityName = $(containerEl).find(".control-label:contains('Entity Name') + .controls-well").text(); if ((typeof entityName === "undefined") || (!entityName)) entityName = null; } return entityName; }, //Checks if the metadata has entity details sections hasEntityDetails: function () { return (this.$(".entitydetails").length > 0); }, /** * Finds the element in the rendered metadata that describes the given data entity. * * @param {(DataONEObject|SolrResult|string)} model - Either a model that represents the data object or the identifier of the data object * @param {Element} [el] - The DOM element to exclusivly search inside. * @return {Element} - The DOM element that describbbes the given data entity. */ findEntityDetailsContainer: function (model, el) { if (!el) var el = this.el; //Get the id and file name for this data object var id = "", fileName = ""; //If a model is given, get the id and file name from the object if (model && (DataONEObject.prototype.isPrototypeOf(model) || SolrResult.prototype.isPrototypeOf(model))) { id = model.get("id"); fileName = model.get("fileName"); } //If a string is given instead, it must be the id of the data object else if (typeof model == "string") { id = model; } //Otherwise, there isn't enough info to find the element, so exit else { return; } //If we already found it earlier, return it now var container = this.$(".entitydetails[data-id='" + id + "'], " + ".entitydetails[data-id='" + DataONEObject.prototype.getXMLSafeID(id) + "']"); if (container.length) return container; //Are we looking for the main object that this MetadataView is displaying? if (id == this.pid) { if (this.$("#Metadata").length > 0) return this.$("#Metadata"); else return this.el; } //Metacat 2.4.2 and up will have the Online Distribution Link marked var link = this.$(".entitydetails a[data-pid='" + id + "']"); //Otherwise, try looking for an anchor with the id matching this object's id if (!link.length) link = $(el).find("a#" + id.replace(/[^A-Za-z0-9]/g, "\\$&")); //Get metadata index view var metadataFromIndex = _.findWhere(this.subviews, { type: "MetadataIndex" }); if (typeof metadataFromIndex === "undefined") metadataFromIndex = null; //Otherwise, find the Online Distribution Link the hard way if ((link.length < 1) && (!metadataFromIndex)) link = $(el).find(".control-label:contains('Online Distribution Info') + .controls-well > a[href*='" + id.replace(/[^A-Za-z0-9]/g, "\\$&") + "']"); if (link.length > 0) { //Get the container element container = $(link).parents(".entitydetails"); if (container.length < 1) { //backup - find the parent of this link that is a direct child of the form element var firstLevelContainer = _.intersection($(link).parents("form").children(), $(link).parents()); //Find the controls-well inside of that first level container, which is the well that contains info about this data object if (firstLevelContainer.length > 0) container = $(firstLevelContainer).children(".controls-well"); if ((container.length < 1) && (firstLevelContainer.length > 0)) container = firstLevelContainer; $(container).addClass("entitydetails"); } //Add the id so we can easily find it later container.attr("data-id", id); return container; } //----Find by file name rather than id----- if (!fileName) { //Get the name of the object first for (var i = 0; i < this.packageModels.length; i++) { var model = _.findWhere(this.packageModels[i].get("members"), { id: id }); if (model) { fileName = model.get("fileName"); break; } } } if (fileName) { var possibleLocations = [".entitydetails [data-object-name='" + fileName + "']", ".entitydetails .control-label:contains('Object Name') + .controls-well:contains('" + fileName + "')", ".entitydetails .control-label:contains('Entity Name') + .controls-well:contains('" + fileName + "')"]; //Search through each possible location in the DOM where the file name might be for (var i = 0; i < possibleLocations.length; i++) { //Get the elements in this view that match the possible location var matches = this.$(possibleLocations[i]); //If exactly one match is found if (matches.length == 1) { //Get the entity details parent element container = $(matches).parents(".entitydetails").first(); //Set the object ID on the element for easier locating later container.attr("data-id", id); if (container.length) break; } } if (container.length) return container; } //--- The last option:---- //If this package has only one item, we can assume the only entity details are about that item var members = this.packageModels[0].get("members"), dataMembers = _.filter(members, function (m) { return (m.get("formatType") == "DATA"); }); if (dataMembers.length == 1) { if (this.$(".entitydetails").length == 1) { this.$(".entitydetails").attr("data-id", id); return this.$(".entitydetails"); } } return false; }, /* * Inserts new image elements into the DOM via the image template. Use for displaying images that are part of this metadata's resource map. */ insertDataDetails: function () { //If there is a metadataIndex subview, render from there. var metadataFromIndex = _.findWhere(this.subviews, { type: "MetadataIndex" }); if (typeof metadataFromIndex !== "undefined") { _.each(this.packageModels, function (packageModel) { metadataFromIndex.insertDataDetails(packageModel); }); return; } var viewRef = this; _.each(this.packageModels, function (packageModel) { var dataDisplay = "", images = [], other = [], packageMembers = packageModel.get("members"); //Don't do this for large packages if (packageMembers.length > 150) return; //==== Loop over each visual object and create a dataDisplay template for it to attach to the DOM ==== _.each(packageMembers, function (solrResult, i) { //Don't display any info about nested packages if (solrResult.type == "Package") return; var objID = solrResult.get("id"); if (objID == viewRef.pid) return; //Is this a visual object (image)? var type = solrResult.type == "SolrResult" ? solrResult.getType() : "Data set"; if (type == "image") images.push(solrResult); //Find the part of the HTML Metadata view that describes this data object var anchor = $(document.createElement("a")).attr("id", objID.replace(/[^A-Za-z0-9]/g, "-")), container = viewRef.findEntityDetailsContainer(objID); var downloadButton = new DownloadButtonView({ model: solrResult }); downloadButton.render(); //Insert the data display HTML and the anchor tag to mark this spot on the page if (container) { //Only show data displays for images hosted on the same origin if (type == "image") { //Create the data display HTML var dataDisplay = $.parseHTML(viewRef.dataDisplayTemplate({ type: type, src: solrResult.get("url"), objID: objID }).trim()); //Insert into the page if ($(container).children("label").length > 0) $(container).children("label").first().after(dataDisplay); else $(container).prepend(dataDisplay); //If this image is private, we need to load it via an XHR request if (!solrResult.get("isPublic")) { //Create an XHR var xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.onload = function () { if (xhr.response) $(dataDisplay).find("img").attr("src", window.URL.createObjectURL(xhr.response)); } //Open and send the request with the user's auth token xhr.open('GET', solrResult.get("url")); xhr.responseType = "blob"; xhr.setRequestHeader("Authorization", "Bearer " + MetacatUI.appUserModel.get("token")); xhr.send(); } } $(container).prepend(anchor); var nameLabel = $(container).find("label:contains('Entity Name')"); if (nameLabel.length) { $(nameLabel).parent().after(downloadButton.el); } } }); //==== Initialize the fancybox images ===== // We will be checking every half-second if all the HTML has been loaded into the DOM - once they are all loaded, we can initialize the lightbox functionality. var numImages = images.length, //The shared lightbox options for both images lightboxOptions = { prevEffect: 'elastic', nextEffect: 'elastic', closeEffect: 'elastic', openEffect: 'elastic', aspectRatio: true, closeClick: true, afterLoad: function () { //Create a custom HTML caption based on data stored in the DOM element viewRef.title = viewRef.title + "
    Download "; }, helpers: { title: { type: 'outside' } } }; if (numImages > 0) { var numImgChecks = 0, //Keep track of how many interval checks we have so we don't wait forever for images to load lightboxImgSelector = "a[class^='fancybox'][data-fancybox-type='image']"; //Add additional options for images var imgLightboxOptions = lightboxOptions; imgLightboxOptions.type = "image"; imgLightboxOptions.perload = 1; var initializeImgLightboxes = function () { numImgChecks++; //Initialize what images have loaded so far after 5 seconds if (numImgChecks == 10) { $(lightboxImgSelector).fancybox(imgLightboxOptions); } //When 15 seconds have passed, stop checking so we don't blow up the browser else if (numImgChecks > 30) { $(lightboxImgSelector).fancybox(imgLightboxOptions); window.clearInterval(imgIntervalID); return; } //Are all of our images loaded yet? if (viewRef.$(lightboxImgSelector).length < numImages) return; else { //Initialize our lightboxes $(lightboxImgSelector).fancybox(imgLightboxOptions); //We're done - clear the interval window.clearInterval(imgIntervalID); } } var imgIntervalID = window.setInterval(initializeImgLightboxes, 500); } }); }, replaceEcoGridLinks: function () { var viewRef = this; //Find the element in the DOM housing the ecogrid link $("a:contains('ecogrid://')").each(function (i, thisLink) { //Get the link text var linkText = $(thisLink).text(); //Clean up the link text var withoutPrefix = linkText.substring(linkText.indexOf("ecogrid://") + 10), pid = withoutPrefix.substring(withoutPrefix.indexOf("/") + 1), baseUrl = MetacatUI.appModel.get('resolveServiceUrl') || MetacatUI.appModel.get('objectServiceUrl'); $(thisLink).attr('href', baseUrl + encodeURIComponent(pid)).text(pid); }); }, publish: function (event) { // target may not actually prevent click events, so double check var disabled = $(event.target).closest("a").attr("disabled"); if (disabled) { return false; } var publishServiceUrl = MetacatUI.appModel.get('publishServiceUrl'); var pid = $(event.target).closest("a").attr("pid"); var ret = confirm("Are you sure you want to publish " + pid + " with a DOI?"); if (ret) { // show the loading icon var message = "Publishing package...this may take a few moments"; this.showLoading(message); var identifier = null; var viewRef = this; var requestSettings = { url: publishServiceUrl + pid, type: "PUT", xhrFields: { withCredentials: true }, success: function (data, textStatus, xhr) { // the response should have new identifier in it identifier = $(data).find("d1\\:identifier, identifier").text(); if (identifier) { viewRef.hideLoading(); var msg = "Published data package '" + identifier + "'. If you are not redirected soon, you can view your published data package here"; viewRef.$el.find('.container').prepend( viewRef.alertTemplate({ msg: msg, classes: 'alert-success' }) ); // navigate to the new view after a few seconds setTimeout( function () { // avoid a double fade out/in viewRef.$el.html(''); viewRef.showLoading(); MetacatUI.uiRouter.navigate("view/" + identifier, { trigger: true }) }, 3000); } }, error: function (xhr, textStatus, errorThrown) { // show the error message, but stay on the same page var msg = "Publish failed: " + $(xhr.responseText).find("description").text(); viewRef.hideLoading(); viewRef.showError(msg); } } $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); } }, //When the given ID from the URL is a resource map that has no metadata, do the following... noMetadata: function (solrResultModel) { this.hideLoading(); this.$el.html(this.template()); this.pid = solrResultModel.get("resourceMap") || solrResultModel.get("id"); //Insert breadcrumbs this.insertBreadcrumbs(); this.insertDataSource(); //Insert a table of contents this.insertPackageTable(solrResultModel); this.renderMetadataFromIndex(); //Insert a message that this data is not described by metadata MetacatUI.appView.showAlert("Additional information about this data is limited since metadata was not provided by the creator.", "alert-warning", this.$(this.metadataContainer)); }, // this will lookup the latest version of the PID showLatestVersion: function () { //If this metadata doc is not obsoleted by a new version, then exit the function if (!this.model.get("obsoletedBy")) { return; } var view = this; //When the latest version is found, this.listenTo(this.model, "change:newestVersion", function () { //Make sure it has a newer version, and if so, if (view.model.get("newestVersion") != view.model.get("id")) { //Put a link to the newest version in the content view.$(".newer-version").replaceWith(view.versionTemplate({ pid: view.model.get("newestVersion") })); } else { view.$(".newer-version").remove(); } }); //Insert the newest version template with a loading message this.$el.prepend(this.versionTemplate({ loading: true })); //Find the latest version of this metadata object this.model.findLatestVersion(); }, showLoading: function (message) { this.hideLoading(); MetacatUI.appView.scrollToTop(); var loading = this.loadingTemplate({ msg: message }); if (!loading) return; this.$loading = $($.parseHTML(loading)); this.$detached = this.$el.children().detach(); this.$el.html(loading); }, hideLoading: function () { if (this.$loading) this.$loading.remove(); if (this.$detached) this.$el.html(this.$detached); }, showError: function (msg) { //Remove any existing error messages this.$el.children(".alert-container").remove(); this.$el.prepend( this.alertTemplate({ msg: msg, classes: 'alert-error', containerClasses: "page", includeEmail: true })); }, /** * When the "Metadata" button in the table is clicked while we are on the Metadata view, * we want to scroll to the anchor tag of this data object within the page instead of navigating * to the metadata page again, which refreshes the page and re-renders (more loading time) **/ previewData: function (e) { //Don't go anywhere yet... e.preventDefault(); //Get the target and id of the click var link = $(e.target); if (!$(link).hasClass("preview")) link = $(link).parents("a.preview"); if (link) { var id = $(link).attr("data-id"); if ((typeof id === "undefined") || !id) return false; //This will make the app defualt to the child view previewData function } else return false; // If we are on the Metadata view, update the URL and scroll to the // anchor window.location.hash = encodeURIComponent(id); MetacatUI.appView.scrollTo(this.findEntityDetailsContainer(id)); return true; }, /** * Try to scroll to the section on a page describing the identifier in the * fragment/hash portion of the current page. * * This function depends on there being an `id` dataset attribute on an * element on the page set to an XML-safe version of the value in the * fragment/hash. Used to provide direct links to sub-resources on a page. */ scrollToFragment: function () { var hash = window.location.hash; if (!hash || hash.length <= 1) { return; } //Get the id from the URL hash and decode it var idFragment = decodeURIComponent(hash.substring(1)); //Find the corresponding entity details section for this id var entityDetailsEl = this.findEntityDetailsContainer(idFragment); if (entityDetailsEl || entityDetailsEl.length) { MetacatUI.appView.scrollTo(entityDetailsEl); } }, /** * Navigate to a new /view URL with a fragment * * Used in getModel() when the pid originally passed into MetadataView * is not a metadata PID but is, instead, a data PID. getModel() does * the work of finding an appropriate metadata PID for the data PID and * this method handles re-routing to the correct URL. * * @param {string} metadata_pid - The new metadata PID * @param {string} data_pid - Optional. A data PID that's part of the * package metadata_pid exists within. */ navigateWithFragment: function (metadata_pid, data_pid) { var next_route = "view/" + encodeURIComponent(metadata_pid); if (typeof data_pid === "string" && data_pid.length > 0) { next_route += "#" + encodeURIComponent(data_pid); } MetacatUI.uiRouter.navigate(next_route, { trigger: true }); }, closePopovers: function (e) { //If this is a popover element or an element that has a popover, don't close anything. //Check with the .classList attribute to account for SVG elements var svg = $(e.target).parents("svg"); if (_.contains(e.target.classList, "popover-this") || ($(e.target).parents(".popover-this").length > 0) || ($(e.target).parents(".popover").length > 0) || _.contains(e.target.classList, "popover") || (svg.length && _.contains(svg[0].classList, "popover-this"))) return; //Close all active popovers this.$(".popover-this.active").popover("hide"); }, highlightNode: function (e) { //Find the id var id = $(e.target).attr("data-id"); if ((typeof id === "undefined") || (!id)) id = $(e.target).parents("[data-id]").attr("data-id"); //If there is no id, return if (typeof id === "undefined") return false; //Highlight its node $(".prov-chart .node[data-id='" + id + "']").toggleClass("active"); //Highlight its metadata section if (MetacatUI.appModel.get("pid") == id) this.$("#Metadata").toggleClass("active"); else { var entityDetails = this.findEntityDetailsContainer(id); if (entityDetails) entityDetails.toggleClass("active"); } }, onClose: function () { var viewRef = this; this.stopListening(); _.each(this.subviews, function (subview) { if (subview.onClose) subview.onClose(); }); this.packageModels = new Array(); this.model.set(this.model.defaults); this.pid = null; this.seriesId = null; this.$detached = null; this.$loading = null; //Put the document title back to the default MetacatUI.appModel.set("title", MetacatUI.appModel.defaults.title); //Remove view-specific classes this.$el.removeClass("container no-stylesheet"); this.$el.empty(); }, /** * Generate a string appropriate to go into the author/creator portion of * a dataset citation from the value stored in the underlying model's * origin field. */ getAuthorText: function () { var authors = this.model.get("origin"), count = 0, authorText = ""; _.each(authors, function (author) { count++; if (count == 6) { authorText += ", et al. "; return; } else if (count > 6) { return; } if (count > 1) { if (authors.length > 2) { authorText += ","; } if (count == authors.length) { authorText += " and"; } if (authors.length > 1) { authorText += " "; } } authorText += author; }); return authorText; }, /** * Generate a string appropriate to be used in the publisher portion of a * dataset citation. This method falls back to the node ID when the proper * node name cannot be fetched from the app's NodeModel instance. */ getPublisherText: function () { var datasource = this.model.get("datasource"), memberNode = MetacatUI.nodeModel.getMember(datasource); if (memberNode) { return memberNode.name; } else { return datasource; } }, /** * Generate a string appropriate to be used as the publication date in a * dataset citation. */ getDatePublishedText: function () { // Dataset/datePublished // Prefer pubDate, fall back to dateUploaded so we have something to show if (this.model.get("pubDate") !== "") { return this.model.get("pubDate") } else { return this.model.get("dateUploaded") } }, /** * Generate Schema.org-compliant JSONLD for the model bound to the view into * the head tag of the page by `insertJSONLD`. * * Note: `insertJSONLD` should be called to do the actual inserting into the * DOM. */ generateJSONLD: function () { var model = this.model; // Determine the path (either #view or view, depending on router // configuration) for use in the 'url' property var href = document.location.href, route = href.replace(document.location.origin + "/", "") .split("/")[0]; // First: Create a minimal Schema.org Dataset with just the fields we // know will come back from Solr (System Metadata fields). // Add the rest in conditional on whether they are present. var elJSON = { "@context": { "@vocab": "https://schema.org/", }, "@type": "Dataset", "@id": "https://dataone.org/datasets/" + encodeURIComponent(model.get("id")), "datePublished": this.getDatePublishedText(), "dateModified": model.get("dateModified"), "publisher": { "@type": "Organization", "name": this.getPublisherText() }, "identifier": this.generateSchemaOrgIdentifier(model.get("id")), "version": model.get("version"), "url": "https://dataone.org/datasets/" + encodeURIComponent(model.get("id")), "schemaVersion": model.get("formatId"), "isAccessibleForFree": true }; // Attempt to add in a sameAs property of we have high confidence the // identifier is a DOI if (this.model.isDOI(model.get("id"))) { var doi = this.getCanonicalDOIIRI(model.get("id")); if (doi) { elJSON["sameAs"] = doi; } } // Second: Add in optional fields // Name if (model.get("title")) { elJSON["name"] = model.get("title") } // Creator if (model.get("origin")) { elJSON["creator"] = model.get("origin").map(function (creator) { return { "@type": "Person", "name": creator }; }); } // Dataset/spatialCoverage if (model.get("northBoundCoord") && model.get("eastBoundCoord") && model.get("southBoundCoord") && model.get("westBoundCoord")) { var spatialCoverage = { "@type": "Place", "additionalProperty": [ { "@type": "PropertyValue", "additionalType": "http://dbpedia.org/resource/Coordinate_reference_system", "name": "Coordinate Reference System", "value": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" } ], "geo": this.generateSchemaOrgGeo(model.get("northBoundCoord"), model.get("eastBoundCoord"), model.get("southBoundCoord"), model.get("westBoundCoord")), "subjectOf": { "@type": "CreativeWork", "fileFormat": "application/vnd.geo+json", "text": this.generateGeoJSONString(model.get("northBoundCoord"), model.get("eastBoundCoord"), model.get("southBoundCoord"), model.get("westBoundCoord")) } }; elJSON.spatialCoverage = spatialCoverage; } // Dataset/temporalCoverage if (model.get("beginDate") && !model.get("endDate")) { elJSON.temporalCoverage = model.get("beginDate"); } else if (model.get("beginDate") && model.get("endDate")) { elJSON.temporalCoverage = model.get("beginDate") + "/" + model.get("endDate"); } // Dataset/variableMeasured if (model.get("attributeName")) { elJSON.variableMeasured = model.get("attributeName"); } // Dataset/description if (model.get("abstract")) { elJSON.description = model.get("abstract"); } else { var datasets_url = "https://dataone.org/datasets/" + encodeURIComponent(model.get("id")); elJSON.description = 'No description is available. Visit ' + datasets_url + ' for complete metadata about this dataset.'; } // Dataset/keywords if (model.get("keywords")) { elJSON.keywords = model.get("keywords").join(", "); } return elJSON; }, /** * Insert Schema.org-compliant JSONLD for the model bound to the view into * the head tag of the page (at the end). * * @param {object} json - JSON-LD to insert into the page * * Some notes: * * - Checks if the JSONLD already exists from the previous data view * - If not create a new script tag and append otherwise replace the text * for the script */ insertJSONLD: function (json) { if (!document.getElementById('jsonld')) { var el = document.createElement('script'); el.type = 'application/ld+json'; el.id = 'jsonld'; el.text = JSON.stringify(json); document.querySelector('head').appendChild(el); } else { var script = document.getElementById('jsonld'); script.text = JSON.stringify(json); } }, /** * Generate a Schema.org/identifier from the model's id * * Tries to use the PropertyValue pattern when the identifier is a DOI * and falls back to a Text value otherwise * * @param {string} identifier - The raw identifier */ generateSchemaOrgIdentifier: function (identifier) { if (!this.model.isDOI()) { return identifier; } var doi = this.getCanonicalDOIIRI(identifier); if (!doi) { return identifier; } return { "@type": "PropertyValue", "propertyID": "https://registry.identifiers.org/registry/doi", "value": doi.replace("https://doi.org/", "doi:"), "url": doi } }, /** * Generate a Schema.org/Place/geo from bounding coordinates * * Either generates a GeoCoordinates (when the north and east coords are * the same) or a GeoShape otherwise. */ generateSchemaOrgGeo: function (north, east, south, west) { if (north === south) { return { "@type": "GeoCoordinates", "latitude": north, "longitude": west } } else { return { "@type": "GeoShape", "box": west + ", " + south + " " + east + ", " + north } } }, /** * Creates a (hopefully) valid geoJSON string from the a set of bounding * coordinates from the Solr index (north, east, south, west). * * This function produces either a GeoJSON Point or Polygon depending on * whether the north and south bounding coordinates are the same. * * Part of the reason for factoring this out, in addition to code * organization issues, is that the GeoJSON spec requires us to modify * the raw result from Solr when the coverage crosses -180W which is common * for datasets that cross the Pacific Ocean. In this case, We need to * convert the east bounding coordinate from degrees west to degrees east. * * e.g., if the east bounding coordinate is 120 W and west bounding * coordinate is 140 E, geoJSON requires we specify 140 E as 220 * * @param {number} north - North bounding coordinate * @param {number} east - East bounding coordinate * @param {number} south - South bounding coordinate * @param {number} west - West bounding coordinate */ generateGeoJSONString: function (north, east, south, west) { if (north === south) { return this.generateGeoJSONPoint(north, east); } else { return this.generateGeoJSONPolygon(north, east, south, west); } }, /** * Generate a GeoJSON Point object * * @param {number} north - North bounding coordinate * @param {number} east - East bounding coordinate * * Example: * { * "type": "Point", * "coordinates": [ * -105.01621, * 39.57422 * ]} */ generateGeoJSONPoint: function (north, east) { var preamble = "{\"type\":\"Point\",\"coordinates\":", inner = "[" + east + "," + north + "]", postamble = "}"; return preamble + inner + postamble; }, /** * Generate a GeoJSON Polygon object from * * @param {number} north - North bounding coordinate * @param {number} east - East bounding coordinate * @param {number} south - South bounding coordinate * @param {number} west - West bounding coordinate * * * Example: * * { * "type": "Polygon", * "coordinates": [[ * [ 100, 0 ], * [ 101, 0 ], * [ 101, 1 ], * [ 100, 1 ], * [ 100, 0 ] * ]} * */ generateGeoJSONPolygon: function (north, east, south, west) { var preamble = "{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\"\:\"Polygon\",\"coordinates\":[["; // Handle the case when the polygon wraps across the 180W/180E boundary if (east < west) { east = 360 - east } var inner = "[" + west + "," + south + "]," + "[" + east + "," + south + "]," + "[" + east + "," + north + "]," + "[" + west + "," + north + "]," + "[" + west + "," + south + "]"; var postamble = "]]}}"; return preamble + inner + postamble; }, /** * Create a canonical IRI for a DOI given a random DataONE identifier. * * @param {string} identifier: The identifier to (possibly) create the IRI * for. * @return {string|null} Returns null when matching the identifier to a DOI * regex fails or a string when the match is successful * * Useful for describing resources identified by DOIs in linked open data * contexts or possibly also useful for comparing two DOIs for equality. * * Note: Really could be generalized to more identifier schemes. */ getCanonicalDOIIRI: function (identifier) { var pattern = /(10\.\d{4,9}\/[-\._;()\/:A-Z0-9]+)$/, match = identifier.match(pattern); if (match === null || match.length !== 2 || match[1].length <= 0) { return null; } return "https://doi.org/" + match[1]; }, /** * Insert citation information as meta tags into the head of the page * * Currently supports Highwire Press style tags (citation_) which is * supposedly what Google (Scholar), Mendeley, and Zotero support. */ insertCitationMetaTags: function () { // Generate template data to use for all templates var title = this.model.get("title"), authors = this.model.get("origin"), publisher = this.getPublisherText(), date = new Date(this.getDatePublishedText()).getUTCFullYear().toString(), isDOI = this.model.isDOI(this.model.get("id")), id = this.model.get("id"), abstract = this.model.get("abstract"); // Generate HTML strings from each template var hwpt = this.metaTagsHighwirePressTemplate({ title: title, authors: authors, publisher: publisher, date: date, isDOI: isDOI, id: id, abstract }); // Clear any that are already in the document. $("meta[name='citation_title']").remove(); $("meta[name='citation_authors']").remove(); $("meta[name='citation_author']").remove(); $("meta[name='citation_publisher']").remove(); $("meta[name='citation_date']").remove(); $("meta[name='citation_doi']").remove(); $("meta[name='citation_abstract']").remove(); // Insert document.head.insertAdjacentHTML("beforeend", hwpt); // Update Zotero // https://www.zotero.org/support/dev/exposing_metadata#force_zotero_to_refresh_metadata document.dispatchEvent(new Event('ZoteroItemUpdated', { bubbles: true, cancelable: true })); }, createAnnotationViews: function () { try { var viewRef = this; _.each($(".annotation"), function (annoEl) { var newView = new AnnotationView({ el: annoEl }); viewRef.subviews.push(newView); }); } catch (e) { console.error(e); } }, insertMarkdownViews: function () { var viewRef = this; _.each($(".markdown"), function (markdownEl) { var newView = new MarkdownView({ markdown: $(markdownEl).text().trim(), el: $(markdownEl).parent() }); viewRef.subviews.push(newView); // Clear out old content before rendering $(markdownEl).remove(); newView.render(); }); } }); return MetadataView; });