define(["jquery", "underscore", "backbone", "uuid",
"models/metadata/eml211/EMLMeasurementScale", "models/metadata/eml211/EMLAnnotation",
"collections/metadata/eml/EMLMissingValueCodes",
"models/DataONEObject"],
function ($, _, Backbone, uuid, EMLMeasurementScale, EMLAnnotation,
EMLMissingValueCodes,
DataONEObject) {
/**
* @class EMLAttribute
* @classdesc EMLAttribute represents a data attribute within an entity, such as
* a column variable in a data table, or a feature attribute in a shapefile.
* @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html
* @classcategory Models/Metadata/EML211
*/
var EMLAttribute = Backbone.Model.extend(
/** @lends EMLAttribute.prototype */{
/* Attributes of an EML attribute object */
defaults: function(){
return {
/* Attributes from EML */
xmlID: null, // The XML id of the attribute
attributeName: null,
attributeLabel: [], // Zero or more human readable labels
attributeDefinition: null,
storageType: [], // Zero or more storage types
typeSystem: [], // Zero or more system types for storage type
measurementScale: null, // An EML{Non}NumericDomain or EMLDateTimeDomain object
missingValueCodes: new EMLMissingValueCodes(), // An EMLMissingValueCodes collection
accuracy: null, // An EMLAccuracy object
coverage: null, // an EMLCoverage object
methods: [], // Zero or more EMLMethods objects
references: null, // A reference to another EMLAttribute by id (needs work)
annotation: [], // Zero or more EMLAnnotation objects
/* Attributes not from EML */
type: "attribute", // The element type in the DOM
parentModel: null, // The parent model this attribute belongs to
objectXML: null, // The serialized XML of this EML attribute
objectDOM: null, // The DOM of this EML attribute
nodeOrder: [ // The order of the top level XML element nodes
"attributeName",
"attributeLabel",
"attributeDefinition",
"storageType",
"measurementScale",
"missingValueCode",
"accuracy",
"coverage",
"methods",
"annotation"
]
}
},
/*
* The map of lower case to camel case node names
* needed to deal with parsing issues with $.parseHTML().
* Use this until we can figure out issues with $.parseXML().
*/
nodeNameMap: {
"attributename": "attributeName",
"attributelabel": "attributeLabel",
"attributedefinition": "attributeDefinition",
"sourced" : "source",
"storagetype": "storageType",
"typesystem": "typeSystem",
"measurementscale": "measurementScale",
"missingvaluecode": "missingValueCode",
"propertyuri": "propertyURI",
"valueuri" : "valueURI"
},
/* Initialize an EMLAttribute object */
initialize: function(attributes, options) {
if (!attributes) {
var attributes = {};
}
// If initialized with missingValueCode as an array, convert it to a collection
if (
attributes.missingValueCodes &&
attributes.missingValueCodes instanceof Array
) {
this.missingValueCodes =
new EMLMissingValueCodes(attributes.missingValueCode);
}
this.stopListening(this.get("missingValueCodes"));
this.listenTo(
this.get("missingValueCodes"),
"update",
this.trickleUpChange
)
this.on(
"change:attributeName " +
"change:attributeLabel " +
"change:attributeDefinition " +
"change:storageType " +
"change:measurementScale " +
"change:missingValueCodes " +
"change:accuracy " +
"change:coverage " +
"change:methods " +
"change:references " +
"change:annotation",
this.trickleUpChange);
},
/*
* Parse the incoming attribute's XML elements
*/
parse: function(attributes, options) {
var $objectDOM;
if ( attributes.objectDOM ) {
$objectDOM = $(attributes.objectDOM);
} else if ( attributes.objectXML ) {
$objectDOM = $(attributes.objectXML);
} else {
return {};
}
// Add the XML id
if ( typeof $objectDOM.attr("id") !== "undefined" ) {
attributes.xmlID = $objectDOM.attr("id");
}
// Add the attributeName
attributes.attributeName = $objectDOM.children("attributename").text();
// Add the attributeLabel
attributes.attributeLabel = [];
var attributeLabels = $objectDOM.children("attributelabel");
_.each(attributeLabels, function(attributeLabel) {
attributes.attributeLabel.push(attributeLabel.textContent);
});
// Add the attributeDefinition
attributes.attributeDefinition = $objectDOM.children("attributedefinition").text();
// Add the storageType
attributes.storageType = [];
attributes.typeSystem = [];
var storageTypes = $objectDOM.children("storagetype");
_.each(storageTypes, function(storageType) {
attributes.storageType.push(storageType.textContent);
var type = $(storageType).attr("typesystem");
attributes.typeSystem.push(type || null);
});
var measurementScale = $objectDOM.find("measurementscale")[0];
if ( measurementScale ) {
attributes.measurementScale =
EMLMeasurementScale.getInstance(measurementScale.outerHTML);
attributes.measurementScale.set("parentModel", this);
}
// Add annotations
var annotations = $objectDOM.children("annotation");
attributes.annotation = [];
_.each(annotations, function(anno) {
annotation = new EMLAnnotation({
objectDOM: anno,
objectXML: anno.outerHTML
}, { parse: true });
attributes.annotation.push(annotation);
}, this);
// Add the missingValueCodes as a collection
attributes.missingValueCodes = new EMLMissingValueCodes();
attributes.missingValueCodes.parse(
$objectDOM.children("missingvaluecode")
);
attributes.objectDOM = $objectDOM[0];
return attributes;
},
serialize: function(){
var objectDOM = this.updateDOM(),
xmlString = objectDOM.outerHTML;
//Camel-case the XML
xmlString = this.formatXML(xmlString);
return xmlString;
},
/* Copy the original XML and update fields in a DOM object */
updateDOM: function(objectDOM){
var nodeToInsertAfter;
var type = this.get("type") || "attribute";
if ( ! objectDOM ) {
objectDOM = this.get("objectDOM");
}
var objectXML = this.get("objectXML");
// If present, use the cached DOM
if ( objectDOM ) {
objectDOM = objectDOM.cloneNode(true);
// otherwise, use the cached XML
} else if ( objectXML ){
objectDOM = $(objectXML)[0].cloneNode(true);
// This is new, create it
} else {
objectDOM = document.createElement(type);
}
// update the id attribute
var xmlID = this.get("xmlID");
if ( xmlID ) {
$(objectDOM).attr("id", xmlID);
}
// Update the attributeName
if ( typeof this.get("attributeName") == "string" && this.get("attributeName").trim().length ) {
if ( $(objectDOM).find("attributename").length ) {
$(objectDOM).find("attributename").text(this.get("attributeName"));
} else {
nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeName");
if( ! nodeToInsertAfter ) {
$(objectDOM).append($(document.createElement("attributename"))
.text(this.get("attributeName"))[0]);
} else {
$(nodeToInsertAfter).after(
$(document.createElement("attributename")).text(this.get("attributeName"))[0]
);
}
}
}
//If there is no attribute name, return an empty string because it
// is invalid
else{
return "";
}
// Update the attributeLabels
nodeToInsertAfter = undefined;
var attributeLabels = this.get("attributeLabel");
if ( attributeLabels ) {
if ( attributeLabels.length ) {
// Copy and reverse the array for inserting
attributeLabels = Array.from(attributeLabels).reverse();
// Remove all current attributeLabels
$(objectDOM).find("attributelabel").remove();
nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeLabel");
if( ! nodeToInsertAfter ) {
// Add the new list back in
_.each(attributeLabels, function(attributeLabel) {
//If there is an empty string or falsey value in the label, don't add it to the XML
// We check purposefuly for falsey types (instead of just doing !attributeLabel) because
// it's ok to serialize labels that are the number 0.
if( (typeof attributeLabel == "string" && !attributeLabel.trim().length) ||
attributeLabel === false || attributeLabel === null || typeof attributeLabel == "undefined"){
return;
}
$(objectDOM).append(
$(document.createElement("attributelabel"))
.text(attributeLabel)[0]);
});
} else {
// Add the new list back in after its previous sibling
_.each(attributeLabels, function(attributeLabel) {
//If there is an empty string or falsey value in the label, don't add it to the XML
// We check purposefuly for falsey types (instead of just doing !attributeLabel) because
// it's ok to serialize labels that are the number 0.
if( (typeof attributeLabel == "string" && !attributeLabel.trim().length) ||
attributeLabel === false || attributeLabel === null || typeof attributeLabel == "undefined"){
return;
}
$(nodeToInsertAfter).after(
$(document.createElement("attributelabel"))
.text(attributeLabel)[0]);
});
}
}
//If the label array is empty, remove all the labels from the DOM
else{
$(objectDOM).find("attributelabel").remove();
}
}
//If there is no attribute label, remove them from the DOM
else{
$(objectDOM).find("attributelabel").remove();
}
// Update the attributeDefinition
nodeToInsertAfter = undefined;
if ( this.get("attributeDefinition") ) {
if ( $(objectDOM).find("attributedefinition").length ) {
$(objectDOM).find("attributedefinition").text(this.get("attributeDefinition"));
} else {
nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeDefinition");
if( ! nodeToInsertAfter ) {
$(objectDOM).append($(document.createElement("attributedefinition"))
.text(this.get("attributeDefinition"))[0]);
} else {
$(nodeToInsertAfter).after($(document.createElement("attributedefinition"))
.text(this.get("attributeDefinition"))[0]);
}
}
}
// If there is no attribute definition, then return an empty String
// because it is invalid
else{
return "";
}
// Update the storageTypes
nodeToInsertAfter = undefined;
var storageTypes = this.get("storageTypes");
if ( storageTypes ) {
if ( storageTypes.length ) {
// Copy and reverse the array for inserting
storageTypes = Array.from(storageTypes).reverse();
// Remove all current attributeLabels
$(objectDOM).find("storagetype").remove();
nodeToInsertAfter = this.getEMLPosition(objectDOM, "storageType");
if( ! nodeToInsertAfter ) {
// Add the new list back in
_.each(storageTypes, function(storageType) {
if(!storageType)
return;
$(objectDOM).append(
$(document.createElement("storagetype"))
.text(storageType)[0]);
});
} else {
// Add the new list back in after its previous sibling
_.each(storageTypes, function(storageType) {
if(!storageType)
return;
$(nodeToInsertAfter).after(
$(document.createElement("storagetype"))
.text(storageType)[0]);
});
}
}
}
/*If there are no storage types, remove them all from the DOM.
TODO: Uncomment this out when storage type is supported in editor
else{
$(objectDOM).find("storagetype").remove();
}
*/
// Update the measurementScale
nodeToInsertAfter = undefined;
var measurementScale = this.get("measurementScale");
var measurementScaleNodes;
var measurementScaleNode;
var domainNode;
if ( typeof measurementScale !== "undefined" && measurementScale) {
// Find the measurementScale child or create a new one
measurementScaleNodes = $(objectDOM).children("measurementscale");
if ( measurementScaleNodes.length ) {
measurementScaleNode = measurementScaleNodes[0];
} else {
measurementScaleNode = document.createElement("measurementscale");
nodeToInsertAfter = this.getEMLPosition(objectDOM, "measurementScale");
if ( typeof nodeToInsertAfter === "undefined" ) {
$(objectDOM).append(measurementScaleNode);
} else {
$(nodeToInsertAfter).after(measurementScaleNode);
}
}
// Append the measurementScale domain content
domainNode = measurementScale.updateDOM();
if (typeof domainNode !== "undefined" ) {
$(measurementScaleNode).children().remove();
$(measurementScaleNode).append(domainNode);
}
} else {
console.log("No measurementScale object has been defined.");
}
// Update annotations
var annotation = this.get("annotation");
// Always remove all annotations to start with
$(objectDOM).children("annotation").remove();
_.each(annotation, function(anno) {
if (anno.isEmpty()) {
return;
}
var after = this.getEMLPosition(objectDOM, "annotation");
$(after).after(anno.updateDOM());
}, this);
// Update the missingValueCodes
nodeToInsertAfter = undefined;
var missingValueCodes = this.get("missingValueCodes");
$(objectDOM).children("missingvaluecode").remove();
if (missingValueCodes) {
var missingValueCodeNodes = missingValueCodes.updateDOM();
if (missingValueCodeNodes) {
nodeToInsertAfter = this.getEMLPosition(objectDOM, "missingValueCode");
if (typeof nodeToInsertAfter === "undefined") {
$(objectDOM).append(missingValueCodeNodes);
} else {
$(nodeToInsertAfter).after(missingValueCodeNodes);
}
}
}
return objectDOM;
},
/*
* Get the DOM node preceding the given nodeName
* to find what position in the EML document
* the named node should be appended
*/
getEMLPosition: function(objectDOM, nodeName) {
var nodeOrder = this.get("nodeOrder");
var position = _.indexOf(nodeOrder, nodeName);
// Append to the bottom if not found
if ( position == -1 ) {
return $(objectDOM).children().last()[0];
}
// Otherwise, go through each node in the node list and find the
// position where this node will be inserted after
for ( var i = position - 1; i >= 0; i-- ) {
if ( $(objectDOM).find(nodeOrder[i].toLowerCase()).length ) {
return $(objectDOM).find(nodeOrder[i].toLowerCase()).last()[0];
}
}
},
formatXML: function(xmlString){
return DataONEObject.prototype.formatXML.call(this, xmlString);
},
validate: function(){
var errors = {};
//If there is no attribute name, add that error message
if(!this.get("attributeName"))
errors.attributeName = "Provide a name for this attribute.";
//If there is no attribute definition, add that error message
if(!this.get("attributeDefinition"))
errors.attributeDefinition = "Provide a definition for this attribute.";
//Get the EML measurement scale model
var measurementScaleModel = this.get("measurementScale");
// If there is no measurement scale model, then add that error message
if( !measurementScaleModel ){
errors.measurementScale = "Choose a measurement scale category for this attribute.";
}
else{
if( !measurementScaleModel.isValid() ){
errors.measurementScale = "More information is needed.";
}
}
// Validate the missing value codes
var missingValueCodesErrors = this.get("missingValueCodes")?.validate();
if (missingValueCodesErrors) {
// Just display the first error message
errors.missingValueCodes = Object.values(missingValueCodesErrors)[0]
}
// If there is a measurement scale model and it is valid and there are no other
// errors, then trigger this model as valid and exit.
if (!Object.keys(errors).length) {
this.trigger("valid", this);
return;
} else {
//If there is at least one error, then return the errors object
return errors;
}
},
/*
* Validates each of the EMLAnnotation models on this model
*
* @return {Array} - Returns an array of error messages for all the EMLAnnotation models
*/
validateAnnotations: function(){
var errors = [];
//Validate each of the EMLAttributes
_.each(this.get("annotation"), function (anno) {
if (anno.isValid()) {
return;
}
errors.push(anno.validationError);
});
return errors;
},
/*
* Climbs up the model heirarchy until it finds the EML model
*
* @return {EML211 or false} - Returns the EML 211 Model or false if not found
*/
getParentEML: function(){
var emlModel = this.get("parentModel"),
tries = 0;
while (emlModel.type !== "EML" && tries < 6){
emlModel = emlModel.get("parentModel");
tries++;
}
if( emlModel && emlModel.type == "EML")
return emlModel;
else
return false;
},
/* Let the top level package know of attribute changes from this object */
trickleUpChange: function(){
MetacatUI.rootDataPackage.packageModel.set("changed", true);
},
createID: function() {
this.set("xmlID", uuid.v4());
}
});
return EMLAttribute;
}
);