diff --git a/src/js/views/AnnotationView.js b/src/js/views/AnnotationView.js
index e9cbbe25b..eac56a15c 100644
--- a/src/js/views/AnnotationView.js
+++ b/src/js/views/AnnotationView.js
@@ -16,6 +16,11 @@ define([
*/
var AnnotationView = Backbone.View.extend(
/** @lends AnnotationView.prototype */ {
+ /**
+ * The type of View this is
+ * @type {string}
+ */
+ type: "AnnotationView",
className: "annotation-view",
annotationPopoverTemplate: _.template(AnnotationPopoverTemplate),
diff --git a/src/js/views/CanonicalDatasetHandlerView.js b/src/js/views/CanonicalDatasetHandlerView.js
new file mode 100644
index 000000000..eb8a35d0a
--- /dev/null
+++ b/src/js/views/CanonicalDatasetHandlerView.js
@@ -0,0 +1,234 @@
+define(["backbone"], (Backbone) => {
+ // The "Type" property of the annotation view
+ const ANNO_VIEW_TYPE = "AnnotationView";
+ // The URI for the schema.org:sameAs annotation
+ const SCHEMA_ORG_SAME_AS = "http://www.w3.org/2002/07/owl#sameAs";
+ // The URI for the prov:wasDerivedFrom annotation
+ const PROV_WAS_DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom";
+
+ // What to call the field that links to the original dataset
+ const CANONICAL_LABEL = "Canonical Dataset";
+ // The text to display in the info tooltip to explain what the canonical
+ // dataset field means
+ const CANONICAL_TOOLTIP_TEXT =
+ "The original dataset this version was derived from. This dataset is essentially a duplicate of the original.";
+
+ // The following properties are used to identify parts of the MetadataView.
+ // If the MetadataView changes, these properties may need to be updated.
+
+ // The name of the property on the MetadataView that contains subviews
+ const SUBVIEW_PROP = "subviews";
+ // Class names used in the MetadataView that we also need to use in this view
+ const METADATA_VIEW_CLASS_NAMES = {
+ fieldItem: "control-group",
+ fieldLabel: "control-label",
+ fieldValue: ["controls", "controls-well"],
+ };
+
+ /**
+ * @class CanonicalDatasetHandlerView
+ * @classdesc A scoped subview responsible for inspecting the rendered DOM
+ * within the MetadataView to identify and highlight the canonical (original)
+ * dataset based on schema.org:sameAs and prov:derivedFrom annotations. This
+ * view modifies specific parts of the MetadataView when a canonical dataset
+ * is detected, providing a clearer distinction between original and derived
+ * datasets.
+ * @classcategory Views
+ * @augments Backbone.View
+ * @class
+ * @since 0.0.0
+ * @screenshot views/CanonicalDatasetHandlerViewView.png TODO
+ */
+ const CanonicalDatasetHandlerView = Backbone.View.extend(
+ /** @lends CanonicalDatasetHandlerView.prototype */
+ {
+ /** @inheritdoc */
+ type: "CanonicalDatasetHandlerView",
+
+ /**
+ * The MetadataView instance this view is scoped to.
+ * @type {MetadataView}
+ */
+ metdataView: null,
+
+ /**
+ * The value of the label to insert the canonical dataset field before.
+ * @type {string}
+ */
+ insertFieldBefore: "Identifier",
+
+ /**
+ * Creates a field item for the MetadataView.
+ * @param {string} label - The label for the field.
+ * @param {string} value - The value for the field.
+ * @param {string} tooltipText - The text to display in the info tooltip.
+ * @param {object} classes - The classes to apply to the field item.
+ * @returns {string} The HTML for the field item.
+ */
+ fieldItemTemplate(label, value, tooltipText, classes) {
+ return `
+
+
${value}
+
`;
+ },
+
+ /**
+ * Initialize the CanonicalDatasetHandlerView.
+ * @param {object} options - A set of options to initialize the view with.
+ * @param {MetadataView} options.metadataView - The MetadataView instance
+ * this view is scoped to. Required.
+ */
+ initialize(options) {
+ this.metadataView = options?.metadataView;
+ if (!this.metadataView) {
+ throw new Error(
+ "The CanonicalDatasetHandlerView requires a MetadataView instance.",
+ );
+ }
+ },
+
+ /** @inheritdoc */
+ render() {
+ if (this.detectCanonicalDataset()) {
+ this.getCitationInfo();
+ this.addFieldItem();
+ this.modifyCitationModal();
+ this.addInfoIcon();
+ this.removeAnnotations();
+ }
+ return this;
+ },
+
+ /**
+ * Inspects the MetadataView DOM to determine if a canonical dataset is
+ * present based on schema.org:sameAs and prov:wasDerivedFrom annotations.
+ * If a canonical dataset is detected, this method sets the appropriate
+ * properties on the view instance.
+ * @returns {boolean} True if a canonical dataset is detected, false
+ * otherwise.
+ */
+ detectCanonicalDataset() {
+ const subViews = this.metadataView[SUBVIEW_PROP];
+ if (!subViews || !subViews.length) {
+ // TODO: Handle no subviews. Wait for subviews to be added?
+ return false;
+ }
+
+ let sameAs = null;
+ let sameAsView = null;
+ let derivedFrom = null;
+ let derivedFromView = null;
+
+ let originalUri = null;
+ let hasCanonical = false;
+
+ // The annotation views provide the URI and value of annotations on the
+ // metadata. We consider the dataset to be canonical if the sameAs and
+ // derivedFrom annotations both point to the same URI.
+ subViews.forEach((view) => {
+ if (view.type !== ANNO_VIEW_TYPE) {
+ return;
+ }
+ const { uri } = view.property;
+ const { value } = view.value;
+ // TODO: handle multiple sameAs/derivedFrom annotations. Unlikely but
+ // technically possible?
+ if (uri === SCHEMA_ORG_SAME_AS) {
+ sameAs = value;
+ sameAsView = view;
+ } else if (uri === PROV_WAS_DERIVED_FROM) {
+ derivedFrom = value;
+ derivedFromView = view;
+ }
+ });
+
+ if (sameAs === derivedFrom && sameAs) {
+ originalUri = sameAs;
+ hasCanonical = true;
+ }
+ this.derivedFromAnnotationView = derivedFromView;
+ this.sameAsAnnotationView = sameAsView;
+ this.canonicalUri = originalUri;
+ this.hasCanonical = hasCanonical;
+ return hasCanonical;
+ },
+
+ /**
+ * Given the canonical dataset URI, fetches citation information for the
+ * canonical dataset, like the title, authors, publication date, etc. Saves
+ * this information in a CitationModel instance.
+ */
+ getCitationInfo() {
+ // TODO: Get citation info from the canonical dataset
+ // what API can we use to get this info?
+ // this.citationModel = new CitationModel({});
+ },
+
+ /**
+ * Removes the sameAs and derivedFrom annotations from the MetadataView.
+ * This is done to prevent redundancy in the metadata display.
+ */
+ removeAnnotations() {
+ this.sameAsAnnotationView?.remove();
+ this.derivedFromAnnotationView?.remove();
+ this.sameAsAnnotationView = null;
+ this.derivedFromAnnotationView = null;
+ },
+
+ /**
+ * Adds a "row" in the MetadataView to display the canonical dataset URI.
+ */
+ addFieldItem() {
+ const { canonicalUri, fieldItemTemplate } = this;
+ const itemHTML = fieldItemTemplate(
+ CANONICAL_LABEL,
+ `${canonicalUri}`,
+ CANONICAL_TOOLTIP_TEXT,
+ METADATA_VIEW_CLASS_NAMES,
+ );
+
+ // Find the parent item that contains the field name the view should
+ // be inserted before
+ const labels = Array.from(
+ this.metadataView.el.querySelectorAll("label"),
+ );
+ const insertBeforeLabel = labels.find(
+ (label) => label.textContent.trim() === this.insertFieldBefore,
+ );
+ // Insert the new field item before the label
+ insertBeforeLabel.parentElement.insertAdjacentHTML(
+ "beforebegin",
+ itemHTML,
+ );
+ },
+
+ /**
+ * Modifies the CitationModalView to add the citation information for the
+ * canonical dataset in addition to the citation information for the
+ * current dataset.
+ */
+ modifyCitationModal() {
+ // TODO
+ },
+
+ /**
+ * Adds a badge to the header of the MetadataView to indicate that the
+ * dataset being displayed is essentially a duplicate of another dataset.
+ */
+ addInfoIcon() {
+ // TODO
+ },
+
+ // TODO: Do we need to remove the view from the DOM when the MetadataView
+ // is removed? Do we need methods to undo the changes made by this view?
+ remove() {},
+ },
+ );
+
+ return CanonicalDatasetHandlerView;
+});
diff --git a/src/js/views/MetadataView.js b/src/js/views/MetadataView.js
index 1bcbed1b4..d0dbc62a3 100644
--- a/src/js/views/MetadataView.js
+++ b/src/js/views/MetadataView.js
@@ -22,6 +22,7 @@ define([
"views/AnnotationView",
"views/MarkdownView",
"views/ViewObjectButtonView",
+ "views/CanonicalDatasetHandlerView",
"text!templates/metadata/metadata.html",
"text!templates/dataSource.html",
"text!templates/publishDOI.html",
@@ -59,6 +60,7 @@ define([
AnnotationView,
MarkdownView,
ViewObjectButtonView,
+ CanonicalDatasetHandlerView,
MetadataTemplate,
DataSourceTemplate,
PublishDoiTemplate,
@@ -189,6 +191,15 @@ define([
this.once("metadataLoaded", () => {
this.createAnnotationViews();
this.insertMarkdownViews();
+ // Modifies the view to indicate that this is a dataset is essentially
+ // a duplicate of another dataset, if applicable
+ if (!this.canonicalDatasetHandler) {
+ // The view should only be created once, but "metadataLoaded" can be
+ // triggered multiple times
+ this.canonicalDatasetHandler = new CanonicalDatasetHandlerView({
+ metadataView: this,
+ }).render();
+ }
});
// Listen to when the package table has been rendered