/*global define */ define(['jquery', 'jqueryui', 'underscore', 'backbone', 'gmaps', 'fancybox', 'clipboard', 'models/PackageModel', 'models/SolrResult', 'views/DownloadButtonView', 'views/ProvChartView', 'views/MetadataIndexView', 'views/ExpandCollapseListView', 'views/ProvStatementView', 'views/PackageTableView', 'views/AnnotatorView', 'views/CitationView', 'text!templates/metadata.html', 'text!templates/dataSource.html', 'text!templates/publishDOI.html', 'text!templates/newerVersion.html', 'text!templates/loading.html', 'text!templates/metadataControls.html', 'text!templates/usageStats.html', 'text!templates/downloadContents.html', 'text!templates/alert.html', 'text!templates/editMetadata.html', 'text!templates/dataDisplay.html', 'text!templates/map.html' ], function($, $ui, _, Backbone, gmaps, fancybox, Clipboard, Package, SolrResult, DownloadButtonView, ProvChart, MetadataIndex, ExpandCollapseList, ProvStatement, PackageTable, AnnotatorView, CitationView, MetadataTemplate, DataSourceTemplate, PublishDoiTemplate, VersionTemplate, LoadingTemplate, ControlsTemplate, UsageTemplate, DownloadContentsTemplate, AlertTemplate, EditMetadataTemplate, DataDisplayTemplate, MapTemplate, AnnotationTemplate) { 'use strict'; var MetadataView = Backbone.View.extend({ subviews: [], pid: null, seriesId: null, model: new SolrResult(), packageModels: new Array(), el: '#Content', metadataContainer: "#metadata-container", citationContainer: "#citation-container", tableContainer: "#table-container", controlsContainer: "#metadata-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), usageTemplate: _.template(UsageTemplate), versionTemplate: _.template(VersionTemplate), loadingTemplate: _.template(LoadingTemplate), controlsTemplate: _.template(ControlsTemplate), dataSourceTemplate: _.template(DataSourceTemplate), downloadContentsTemplate: _.template(DownloadContentsTemplate), editMetadataTemplate: _.template(EditMetadataTemplate), dataDisplayTemplate: _.template(DataDisplayTemplate), mapTemplate: _.template(MapTemplate), 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" }, initialize: function (options) { if((options === undefined) || (!options)) var options = {}; this.pid = options.pid || options.id || appModel.get("pid") || null; if(typeof options.el !== "undefined") this.setElement(options.el); }, // Render the main metadata view render: function () { this.stopListening(); 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 = appModel.get("pid"); this.listenTo(appUserModel, "change:loggedIn", this.render); this.getModel(); return this; }, /* * 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 = model; this.renderMetadata(); } else if(this.model.get("formatType") == "DATA"){ if(this.model.get("isDocumentedBy")){ //Get the metadata pid that documents this data object this.pid = _.first(this.model.get("isDocumentedBy")); //Reset the model this.model.set( this.model.defaults ); this.model.set("id", this.pid); //Retrieve the model this.getModel(this.pid); 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")); }); this.listenToOnce(model, "404", this.showNotFound); 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 controls this.insertControls(); this.insertOwnerControls(); //Show loading icon in metadata section this.$(this.metadataContainer).html(this.loadingTemplate({ msg: "Retrieving metadata ..." })); // Check for a view service in this appModel if((appModel.get('viewServiceUrl') !== undefined) && (appModel.get('viewServiceUrl'))) var endpoint = appModel.get('viewServiceUrl') + encodeURIComponent(pid); if(endpoint && (typeof endpoint !== "undefined")){ var viewRef = this; var loadSettings = { url: endpoint, success: function(response, status, xhr) { //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("indexed")) viewRef.$(viewRef.metadataContainer).prepend(viewRef.alertTemplate({ msg: "There is limited metadata about this dataset since it has been archived." })); //viewRef.insertDataSource(); 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, appUserModel.createAjaxSettings())); } else this.renderMetadataFromIndex(); }, /* 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 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", "#") .addClass("home") .text("Home"))) .append($(document.createElement("li")) .addClass("search") .append($(document.createElement("a")) .attr("href", "#data" + ((appModel.get("page") > 0)? ("/page/" + (parseInt(appModel.get("page"))+1)) : "")) .addClass("search") .text("Search"))) .append($(document.createElement("li")) .append($(document.createElement("a")) .attr("href", "#" + Backbone.history.fragment) .addClass("inactive") .text("Metadata"))); if(uiRouter.lastRoute() == "data"){ $(breadcrumbs).prepend($(document.createElement("a")) .attr("href", "#data/page/" + appModel.get("page")) .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); }, showNotFound: function(){ //If we haven't checked the logged-in status of the user yet, wait a bit until we show a 404 msg, in case this content is their private content if(!appUserModel.get("checked")){ this.listenToOnce(appUserModel, "change:checked", this.showNotFound); return; } if(!this.model.get("notFound")) return; var msg = "

Nothing was found for one of the following reasons:

" + ""; this.hideLoading(); this.showError(msg); }, getPackageDetails: function(packageIDs){ var viewRef = this; 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 viewRef.listenToOnce(thisPackage, "change:parentPackageMetadata", viewRef.insertParentLink); //When the package info is fully retrieved viewRef.listenToOnce(thisPackage, 'complete', function(thisPackage){ //When all packages are fully retrieved completePackages++; if(completePackages >= packageIDs.length){ var latestPackages = _.filter(viewRef.packageModels, function(m){ return !_.contains(packageIDs, m.get("obsoletedBy")); }); viewRef.packageModels = latestPackages; viewRef.insertPackageDetails(latestPackages); } }); //Save the package in the view viewRef.packageModels.push(thisPackage); //Get the members thisPackage.getMembers({getParentMetadata: true }); }); } }, 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(); }, /* * 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; } var viewRef = this; if(!packages) var packages = this.packageModels; //Format this variable as an array if( !Array.isArray(packages) ) packages = [packages]; //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(); if(nestedPckgs.length > 0){ var title = 'Current Data Set (1 of ' + (nestedPckgs.length + 1) + ') Package: ' + packageModel.get("id") + ''; viewRef.insertPackageTable(packageModel, { title: title }); _.each(nestedPckgs, function(nestedPackage, i, list){ var title = 'Nested Data Set (' + (i+2) + ' of ' + (list.length+1) + ') Package: ' + nestedPackage.get("id") + ' (View ) '; viewRef.insertPackageTable(nestedPackage, { title: title, nested: true }); }); } else{ var title = packageModel.get("id") ? 'Package: ' + packageModel.get("id") + '' : ""; title = "Files in this dataset " + title; viewRef.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(); //Show the provenance trace for this package if(packageModel.get("provenanceFlag") == "complete") viewRef.drawProvCharts(packageModel); else{ viewRef.listenToOnce(packageModel, "change:provenanceFlag", viewRef.drawProvCharts); packageModel.getProvTrace(); } }); //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; viewRef.insertPackageTable(packageModel); } //Insert the data details sections this.insertDataDetails(); //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 }); //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 $(this.tableContainer).children(".loading").remove(); $(tableContainer).append(tableView.render().el); $(tableContainer).find(".tooltip-this").tooltip(); this.subviews.push(tableView); }, 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", "#view/" + 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 -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 = ""; //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 || !nodeModel || !nodeModel.get("members").length || !this.$(this.dataSourceContainer).length) return; var dataSource = nodeModel.getMember(this.model), replicaMNs = 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 = "

" + 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); }); } }, /* * 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(){ if( !appModel.get("publishServiceUrl") ) return false; //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")) return false; //Insert the controls container var controlsEl = $(document.createElement("div")).addClass("authority-controls inline-buttons"); $(container).html(controlsEl); //Insert an Edit button controlsEl.append( viewRef.editMetadataTemplate({ identifier: pid })); //Insert a Publish button if its not already published with a DOI if(!model.isDOI()){ //Insert the template controlsEl.append( viewRef.doiTemplate({ isAuthorized: true, identifier: pid })); } //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; //Listen for changes to the authorization flag //packageModel.once("change:isAuthorized", viewRef.createProvEditor, viewRef); //packageModel.once("sync", viewRef.createProvEditor, viewRef); //Now get the RDF XML and check for the user's authority on this resource map //packageModel.fetch(); //packageModel.checkAuthority(); }); 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: * * * * 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(){ //Get template var controlsContainer = this.controlsTemplate({ citation: $(this.citationContainer).text(), url: window.location, mdqUrl: appModel.get("mdqUrl"), model: this.model.toJSON() }); $(this.controlsContainer).html(controlsContainer); var view = this; //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){ $(e.trigger).siblings(".copy-success").show().delay(1000).fadeOut(); }); 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(); }, /* * 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(packageModel){ //Provenance has to be retrieved from the Package Model (getProvTrace()) before the charts can be drawn if(packageModel.get("provenanceFlag") != "complete") return false; var view = this; //Draw two flow charts to represent the sources and derivations at a package level var packageSources = packageModel.get("sourcePackages"), packageDerivations = packageModel.get("derivationPackages"); if(Object.keys(packageSources).length){ var sourceProvChart = new ProvChart({ sources : packageSources, context : packageModel, contextEl : this.$(this.articleContainer), packageModel : packageModel, 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 : packageModel, contextEl : this.$(this.articleContainer), packageModel : packageModel, parentView : view }); this.subviews.push(derivationProvChart); this.$(this.articleContainer).after(derivationProvChart.render().el); } if(packageModel.get("sources").length || packageModel.get("derivations").length){ //Draw the provenance charts for each member of this package at an object level _.each(packageModel.get("members"), function(member, i){ var entityDetailsSection = view.findEntityDetailsContainer(member.get("id")); //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(memberSources.length){ var memberSourcesProvChart = new ProvChart({ sources : memberSources, context : member, contextEl : entityDetailsSection, packageModel : packageModel, parentView : view }); view.subviews.push(memberSourcesProvChart); $(entityDetailsSection).before(memberSourcesProvChart.render().el); view.$(view.articleContainer).addClass("gutters"); } if(memberDerivations.length){ //Make the derivation chart for this member var memberDerivationsProvChart = new ProvChart({ derivations : memberDerivations, context : member, contextEl : entityDetailsSection, packageModel : packageModel, parentView : view }); 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 > 0){ 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 + "']"); //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++; } }); } }, /* * Creates a provenance editor */ createProvEditor: function(){ //Get the package - just get the first one for now //TODO: Make sure this is the parent resource map var packageModel = this.packageModels[0]; //If this user is not authorized to edit this resource map, then exit //Or if this is package hasn't been retrieved yet, then exit if(!packageModel.get("id") || !packageModel.get("isAuthorized") || !packageModel.get("objectXML")) return; //Render the prov charts in the gutters _.each(this.$(".entitydetails"), function(entityDetailsEl){ //If this section doesn't have a prov chart already, create a new blank one if(!$(entityDetailsEl).is(".hasProvLeft") || !$(entityDetailsEl).is(".hasProvRight")){ $(entityDetailsEl).parent().addClass("gutters"); //Get the id of this entity var entityId = $(entityDetailsEl).attr("data-id"), model, packageModel; //Get the model for this entity and its package model findMember: for(var i=0; i 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: 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.get("id"), 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, 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.get("id")); 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); }, findEntityDetailsContainer: function(id, el){ if(!el) var el = this.el; //If we already found it earlier, return it now var container = this.$(".entitydetails[data-id='" + 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 + "']"); 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 //Get the name of the object first var name = ""; for(var i=0; i -1)){ name = name.substring(0, name.lastIndexOf(".")); matches = entityNames.find("strong:contains('" + name + "')"); } //If we found more than one match, filter out the substring matches if(matches.length > 1){ matches = _.filter(matches, function(div){ return (div.textContent == name); }); } if(matches.length){ container = $(matches).parents(".entitydetails").first(); container.attr("data-id", id); return container; } } } //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){ if((type == "image") || (type == "PDF")){ //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 " + 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 + " Download "; }, 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; iDownload "; } } 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 = appModel.get('resolveServiceUrl') || 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 = 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(); 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, 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 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() { 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.$el.prepend(view.versionTemplate({pid: view.model.get("newestVersion")})); }); //Find the latest version of this metadata object this.model.findLatestVersion(); }, showLoading: function(message) { this.hideLoading(); 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){ this.$el.prepend( this.alertTemplate({ msg: msg, classes: 'alert-error', containerClasses: "page", includeEmail: true })); }, setUpAnnotator: function() { if(!appModel.get("annotatorUrl")) return; var annotator = new AnnotatorView({ parentView: this }); this.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, then let's scroll to the anchor appView.scrollTo(this.findEntityDetailsContainer(id)); return 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(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.el != viewRef.el) subview.remove(); }); 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 appModel.set("title", appModel.defaults.title); //Remove view-specific classes this.$el.removeClass("container no-stylesheet"); this.$el.empty(); } }); return MetadataView; });