define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
$,
_,
Backbone,
DataONEObject,
) {
/**
* @class EMLNumericDomain
* @classdesc EMLNumericDomain represents the measurement scale of an interval
* or ratio measurement scale attribute, and is an extension of
* EMLMeasurementScale.
* @classcategory Models/Metadata/EML211
* @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_measurementScale
* @extends Backbone.Model
*/
var EMLNumericDomain = Backbone.Model.extend(
/** @lends EMLNumericDomain.prototype */ {
type: "EMLNumericDomain",
/* Attributes of an EMLNonNumericDomain object */
defaults: function () {
return {
/* Attributes from EML, extends attributes from EMLMeasurementScale */
measurementScale: null, // the required name of this measurement scale
unit: null, // the required standard or custom unit definition
precision: null, // the precision of the observed number
numericDomain: {}, // a required numeric domain object or its reference
};
},
/**
* 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: {
standardunit: "standardUnit",
customunit: "customUnit",
numericdomain: "numericDomain",
numbertype: "numberType",
},
/* Initialize an EMLNonNumericDomain object */
initialize: function (attributes, options) {
this.on("change:numericDomain", this.trickleUpChange);
},
/**
* Parse the incoming measurementScale's XML elements
*/
parse: function (attributes, options) {
var $objectDOM;
var measurementScale;
var rootNodeName;
if (attributes.objectDOM) {
rootNodeName = $(attributes.objectDOM)[0].localName;
$objectDOM = $(attributes.objectDOM);
} else if (attributes.objectXML) {
rootNodeName = $(attributes.objectXML)[0].localName;
$objectDOM = $($(attributes.objectXML)[0]);
} else {
return {};
}
// do we have an appropriate measurementScale tree?
var index = _.indexOf(
["measurementscale", "interval", "ratio"],
rootNodeName,
);
if (index == -1) {
throw new Error(
"The measurement scale XML does not have a root " +
"node of 'measurementScale', 'interval', or 'ratio'.",
);
}
// If measurementScale is present, add it
if (rootNodeName == "measurementscale") {
attributes.measurementScale = $objectDOM
.children()
.first()[0].localName;
$objectDOM = $objectDOM.children().first();
} else {
attributes.measurementScale = $objectDOM.localName;
}
// Add the unit
var unitObject = {};
var unit = $objectDOM.children("unit");
var standardUnitNodes = unit.children("standardunit"),
customUnitNodes = unit.children("customunit"),
standardUnit,
customUnit;
if (standardUnitNodes.length) {
standardUnit = standardUnitNodes.text();
if (standardUnit) unitObject.standardUnit = standardUnit;
} else if (customUnitNodes.length) {
customUnit = customUnitNodes.text();
if (customUnit) unitObject.customUnit = customUnit;
}
attributes.unit = unitObject;
// Add the precision
var precision = $objectDOM.children("precision").text();
if (precision) {
attributes.precision = precision;
}
// Add the numericDomain
var numericDomainObject = {};
var numericDomain = $objectDOM.children("numericdomain");
var numberType;
var boundsArray = [];
var boundsObject;
var bounds;
var minimum;
var maximum;
var references;
if (numericDomain) {
// Add the XML id of the numeric domain
if ($(numericDomain).attr("id")) {
numericDomainObject.xmlID = $(numericDomain).attr("id");
}
// Add the numberType
numberType = $(numericDomain).children("numbertype");
if (numberType) {
numericDomainObject.numberType = numberType.text();
// Add optional bounds
bounds = $(numericDomain).children("bounds");
if (bounds.length) {
_.each(bounds, function (bound) {
boundsObject = {}; // initialize on each
minimum = $(bound).children("minimum").text();
maximum = $(bound).children("maximum").text();
if (minimum && maximum) {
boundsObject.minimum = minimum;
boundsObject.maximum = maximum;
} else if (minimum) {
boundsObject.minimum = minimum;
} else if (maximum) {
boundsObject.maximum = maximum;
}
// If one of min or max is defined, add to the bounds array
if (boundsObject.minimum || boundsObject.maximum) {
boundsArray.push(boundsObject);
}
});
}
numericDomainObject.bounds = boundsArray;
} else {
// Otherwise look for references
references = $(numericDomain).children("references");
if (references) {
numericDomainObject.references = references.text();
}
}
attributes.numericDomain = numericDomainObject;
}
attributes.objectDOM = $objectDOM[0];
return attributes;
},
/* Serialize the model to XML */
serialize: function () {
var objectDOM = this.updateDOM();
var xmlString = objectDOM.outerHTML;
// Camel-case the XML
xmlString = this.formatXML(xmlString);
return xmlString;
},
/* Copy the original XML DOM and update it with new values from the model */
updateDOM: function (objectDOM) {
var nodeToInsertAfter;
var type = this.get("measurementScale");
if (typeof type === "undefined") {
console.warn("Defaulting to an interval measurementScale.");
type = "interval";
}
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 unit
var unit = this.get("unit");
var unitNode;
var unitTypeNode;
if (unit) {
// Remove any existing unit
$(objectDOM).find("unit").remove();
// Build a unit element, and populate a standard or custom child
unitNode = document.createElement("unit");
if (typeof unit.standardUnit !== "undefined") {
unitTypeNode = document.createElement("standardUnit");
$(unitTypeNode).text(unit.standardUnit);
} else if (typeof unit.customUnit !== "undefined") {
unitTypeNode = document.createElement("customUnit");
$(unitTypeNode).text(unit.customUnit);
} else {
// Hmm, unit isn't an object?
// Default to a standard unit
unitTypeNode = document.createElement("standardUnit");
if (typeof unit === "string") {
$(unitTypeNode).text(unit);
console.warn("EMLNumericDomain.unit should be an object.");
} else {
// We're really striking out. Default to dimensionless.
$(unitTypeNode).text("dimensionless");
console.warn(
"Defaulting EMLNumericDomain.unit to dimensionless.",
);
}
}
$(unitNode).append(unitTypeNode);
// Add the unit to the DOM
nodeToInsertAfter = this.getEMLPosition(objectDOM, "unit");
if (!nodeToInsertAfter) {
$(objectDOM).prepend(unitNode);
} else {
$(nodeToInsertAfter).after(unitNode);
}
}
// Update the precision
if (this.get("precision")) {
if ($(objectDOM).find("precision").length) {
$(objectDOM).find("precision").text(this.get("precision"));
} else {
nodeToInsertAfter = this.getEMLPosition(objectDOM, "precision");
if (!nodeToInsertAfter) {
$(objectDOM).append(
$(document.createElement("precision")).text(
this.get("precision"),
)[0],
);
} else {
$(nodeToInsertAfter).after(
$(document.createElement("precision")).text(
this.get("precision"),
)[0],
);
}
}
}
// Update the numericDomain
var numericDomain = this.get("numericDomain");
var numericDomainNode = $(objectDOM).find("numericdomain")[0];
var numberType;
var numberTypeNode;
var minBound;
var maxBound;
var boundsNode;
var minBoundNode;
var maxBoundNode;
if (numericDomain) {
var oldNumericDomainNode = $(numericDomainNode).clone();
// Remove the existing numericDomainNode node
if (typeof numericDomainNode !== "undefined") {
numericDomainNode.remove();
}
// Build the new numericDomain node
numericDomainNode = document.createElement("numericdomain");
// Do we have numberType?
if (typeof numericDomain.numberType !== "undefined") {
numberTypeNode = document.createElement("numbertype");
$(numberTypeNode).text(numericDomain.numberType);
$(numericDomainNode).append(numberTypeNode);
}
// Do we have bounds?
if (
typeof numericDomain.bounds !== "undefined" &&
numericDomain.bounds.length
) {
_.each(numericDomain.bounds, function (bound) {
minBound = bound.minimum;
maxBound = bound.maximum;
boundsNode = document.createElement("bounds");
var hasBounds =
typeof minBound !== "undefined" ||
typeof maxBound !== "undefined";
if (hasBounds) {
// Populate the minimum element
if (typeof minBound !== "undefined") {
minBoundNode = $(document.createElement("minimum"));
minBoundNode.text(minBound);
var existingExclusive = oldNumericDomainNode
.find("minimum")
.attr("exclusive");
if (!existingExclusive || existingExclusive === "false")
minBoundNode.attr("exclusive", "false");
else minBoundNode.attr("exclusive", "true");
}
// Populate the maximum element
if (typeof maxBound !== "undefined") {
maxBoundNode = $(document.createElement("maximum"));
maxBoundNode.text(maxBound);
var existingExclusive = oldNumericDomainNode
.find("maximum")
.attr("exclusive");
if (!existingExclusive || existingExclusive === "false")
maxBoundNode.attr("exclusive", "false");
else maxBoundNode.attr("exclusive", "true");
}
$(boundsNode).append(minBoundNode, maxBoundNode);
$(numericDomainNode).append(boundsNode);
} else {
// Do nothing. Content is missing, don't append the node
}
});
} else {
// Basically do nothing. Don't append the numericDomain element
// TODO: handle numericDomain.references
}
nodeToInsertAfter = this.getEMLPosition(objectDOM, "numericDomain");
if (!nodeToInsertAfter) {
$(objectDOM).append(numericDomainNode);
} else {
$(nodeToInsertAfter).after(numericDomainNode);
}
}
return objectDOM;
},
formatXML: function (xmlString) {
return DataONEObject.prototype.formatXML.call(this, xmlString);
},
/**/
getEMLPosition: function (objectDOM, nodeName) {
// TODO: set the node order
var nodeOrder = ["unit", "precision", "numericDomain"];
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]).length) {
return $(objectDOM).find(nodeOrder[i]).last()[0];
}
}
},
validate: function () {
var errors = {};
if (!this.get("unit")) errors.unit = "Choose a unit.";
if (Object.keys(errors).length) return errors;
else {
this.trigger("valid");
return;
}
},
/*
* 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);
},
},
);
return EMLNumericDomain;
});