/* global define */
define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'],
function ($, _, Backbone, DataONEObject) {
/**
* @class EMLGeoCoverage
* @classdesc A description of geographic coverage of a dataset, per the EML 2.1.1 metadata standard
* @classcategory Models/Metadata/EML211
* @extends Backbone.Model
* @constructor
*/
var EMLGeoCoverage = Backbone.Model.extend(
/** @lends EMLGeoCoverage.prototype */{
defaults: {
objectXML: null,
objectDOM: null,
parentModel: null,
description: null,
east: null,
north: null,
south: null,
west: null
},
initialize: function (attributes) {
if (attributes && attributes.objectDOM) this.set(this.parse(attributes.objectDOM));
//specific attributes to listen to
this.on("change:description " +
"change:east " +
"change:west " +
"change:south " +
"change:north",
this.trickleUpChange);
},
/*
* Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
* Used during parse() and serialize()
*/
nodeNameMap: function () {
return {
"altitudemaximum": "altitudeMaximum",
"altitudeminimum": "altitudeMinimum",
"altitudeunits": "altitudeUnits",
"boundingaltitudes": "boundingAltitudes",
"boundingcoordinates": "boundingCoordinates",
"eastboundingcoordinate": "eastBoundingCoordinate",
"geographiccoverage": "geographicCoverage",
"geographicdescription": "geographicDescription",
"northboundingcoordinate": "northBoundingCoordinate",
"southboundingcoordinate": "southBoundingCoordinate",
"westboundingcoordinate": "westBoundingCoordinate"
}
},
/** Based on this example serialization
<geographicCoverage scope="document">
<geographicDescription>Rhine-Main-Observatory</geographicDescription>
<boundingCoordinates>
<westBoundingCoordinate>9.0005</westBoundingCoordinate>
<eastBoundingCoordinate>9.0005</eastBoundingCoordinate>
<northBoundingCoordinate>50.1600</northBoundingCoordinate>
<southBoundingCoordinate>50.1600</southBoundingCoordinate>
</boundingCoordinates>
</geographicCoverage>
**/
parse: function (objectDOM) {
var modelJSON = {};
if (!objectDOM) {
if (this.get("objectDOM"))
var objectDOM = this.get("objectDOM");
else
return {};
}
//Create a jQuery object of the DOM
var $objectDOM = $(objectDOM);
//Get the geographic description
modelJSON.description = $objectDOM.children('geographicdescription').text();
//Get the bounding coordinates
var boundingCoordinates = $objectDOM.children('boundingcoordinates');
if (boundingCoordinates) {
modelJSON.east = boundingCoordinates.children('eastboundingcoordinate').text().replace("+", "");
modelJSON.north = boundingCoordinates.children('northboundingcoordinate').text().replace("+", "");
modelJSON.south = boundingCoordinates.children('southboundingcoordinate').text().replace("+", "");
modelJSON.west = boundingCoordinates.children('westboundingcoordinate').text().replace("+", "");
}
return modelJSON;
},
serialize: function () {
var objectDOM = this.updateDOM(),
xmlString = objectDOM.outerHTML;
//Camel-case the XML
xmlString = this.formatXML(xmlString);
return xmlString;
},
/*
* Makes a copy of the original XML DOM and updates it with the new values from the model.
*/
updateDOM: function () {
var objectDOM;
if (!this.isValid()) {
return "";
}
if (this.get("objectDOM")) {
objectDOM = $(this.get("objectDOM").cloneNode(true));
} else {
objectDOM = $(document.createElement("geographiccoverage"));
}
//If only one point is given, make sure both points are the same
if ((this.get("north") && this.get("west")) && (!this.get("south") && !this.get("east"))) {
this.set("south", this.get("north"));
this.set("east", this.get("west"));
}
else if ((this.get("south") && this.get("east")) && (!this.get("north") && !this.get("west"))) {
this.set("north", this.get("south"));
this.set("west", this.get("east"));
}
// Description
if (!objectDOM.children("geographicdescription").length)
objectDOM.append($(document.createElement("geographicdescription")).text(this.get("description")));
else
objectDOM.children("geographicdescription").text(this.get("description"));
// Create the bounding coordinates element
var boundingCoordinates = objectDOM.find("boundingcoordinates");
if (!boundingCoordinates.length) {
boundingCoordinates = document.createElement("boundingcoordinates");
objectDOM.append(boundingCoordinates);
}
//Empty out the coordinates first
$(boundingCoordinates).empty();
//Add the four coordinate values
$(boundingCoordinates).append($(document.createElement("westboundingcoordinate")).text(this.get("west")),
$(document.createElement("eastboundingcoordinate")).text(this.get("east")),
$(document.createElement("northboundingcoordinate")).text(this.get("north")),
$(document.createElement("southboundingcoordinate")).text(this.get("south")));
return objectDOM;
},
/**
* Sometimes we'll need to add a space between error messages, but only if an
* error has already been triggered. Use addSpace to accomplish this.
*
* @param {string} msg The string that will be appended
* @param {bool} front A flag that when set will append the whitespace to the front of 'msg'
* @return {string} The string that was passed in, 'msg', with whitespace appended
*/
addSpace: function (msg, front) {
if (typeof front === "undefined") {
front = false;
}
if (msg) {
if (front) {
return (" " + msg);
}
return msg += " ";
}
return msg;
},
/**
* Because the same error messages are used in a couple of different places, we centralize the strings
* and access here.
*
* @param {string} area Specifies the area that the error message belongs to.
* Browse through the switch statement to find the one you need.
* @return {string} The error message
*/
getErrorMessage: function (area) {
switch (area) {
case "north":
return "The Northwest latitude must be between -90 and 90.";
break;
case "east":
return "The Southeast longitude must be between -180 and 180.";
break;
case "south":
return "The Southeast latitude must be between -90 and 90.";
break;
case "west":
return "The Northwest longitude must be between -180 and 180.";
break;
case "missing":
return "Each coordinate must include a latitude AND longitude.";
break;
case "description":
return "Each location must have a description.";
break;
case "needPair":
return "Each location description must have at least one coordinate pair.";
break;
default:
return "";
break;
}
},
/**
* Generates an object that describes the current state of each latitude
* and longitude box. The status includes whether there is a value and
* if the value is valid.
*
* @return {array} An array containing the current state of each coordinate box
*/
getCoordinateStatus: function () {
var north = this.get("north"),
east = this.get("east"),
south = this.get("south"),
west = this.get("west");
return {
'north': {
isSet: typeof north !== "undefined" && north != null && north !== "",
isValid: this.validateCoordinate(north, -90, 90)
},
'east': {
isSet: typeof east !== "undefined" && east != null && east !== "",
isValid: this.validateCoordinate(east, -180, 180)
},
'south': {
isSet: typeof south !== "undefined" && south != null && south !== "",
isValid: this.validateCoordinate(south, -90, 90)
},
'west': {
isSet: typeof west !== "undefined" && west != null && west !== "",
isValid: this.validateCoordinate(west, -180, 180)
},
}
},
/**
* Checks the status object for conditions that warrant an error message to the user. This is called
* during the validation processes (validate() and updateModel()) after the status object has been
* created by getCoordinateStatus().
*
* @param status The status object, holding the state of the coordinates
* @return {string} Any errors that need to be displayed to the user
*/
generateStatusErrors: function (status) {
var errorMsg = "";
// Northwest Latitude
if (status.north.isSet && !status.north.isValid) {
errorMsg = this.addSpace(errorMsg);
errorMsg += this.getErrorMessage("north");
}
// Northwest Longitude
if (status.west.isSet && !status.west.isValid) {
errorMsg = this.addSpace(errorMsg);
errorMsg += this.getErrorMessage("west");
}
// Southeast Latitude
if (status.south.isSet && !status.south.isValid) {
errorMsg = this.addSpace(errorMsg);
errorMsg += this.getErrorMessage("south");
}
// Southeast Longitude
if (status.east.isSet && !status.east.isValid) {
errorMsg = this.addSpace(errorMsg);
errorMsg += this.getErrorMessage("east");
}
return errorMsg;
},
/**
* This grabs the various location elements and validates the user input. In the case of an error,
* we append an error string (errMsg) so that we display all of the messages at the same time. This
* validates the entire location row by adding extra checks for a description and for coordinate pairs
*
* @return {string} The error messages that the user will see
*/
validate: function () {
var errors = {};
if (!this.get("description")) {
errors.description = this.getErrorMessage("description");
}
var pointStatuses = this.getCoordinateStatus();
/*
if (!this.checkForPairs(pointStatuses)) {
errorMsg = this.addSpace(errorMsg);
errorMsg += this.getErrorMessage("needPair");
}
if( this.hasMissingPoint(pointStatuses) ) {
//errorMsg = this.addSpace(errorMsg);
errors += this.getErrorMessage("missing");
}
*/
// errorMsg += this.addSpace(this.generateStatusErrors(pointStatuses), true);
if( !pointStatuses.north.isSet && !pointStatuses.south.isSet &&
!pointStatuses.east.isSet && !pointStatuses.west.isSet){
errors.north = this.getErrorMessage("needPair");
errors.west = "";
}
//Check that all the values are correct
if( pointStatuses.north.isSet && !pointStatuses.north.isValid )
errors.north = this.getErrorMessage("north");
if( pointStatuses.south.isSet && !pointStatuses.south.isValid )
errors.south = this.getErrorMessage("south");
if( pointStatuses.east.isSet && !pointStatuses.east.isValid )
errors.east = this.getErrorMessage("east");
if( pointStatuses.west.isSet && !pointStatuses.west.isValid )
errors.west = this.getErrorMessage("west");
if( pointStatuses.north.isSet && !pointStatuses.west.isSet )
errors.west = this.getErrorMessage("missing");
else if( !pointStatuses.north.isSet && pointStatuses.west.isSet )
errors.north = this.getErrorMessage("missing");
else if( pointStatuses.south.isSet && !pointStatuses.east.isSet )
errors.east = this.getErrorMessage("missing");
else if( !pointStatuses.south.isSet && pointStatuses.east.isSet )
errors.south = this.getErrorMessage("missing");
if( Object.keys(errors).length )
return errors;
else
return false;
},
/**
* Checks for any coordinates with missing counterparts.
*
* @param status The status of the coordinates
* @return {bool} True if there are missing coordinates, false otherwise
*/
hasMissingPoint: function (status) {
if ((status.north.isSet && !status.west.isSet) ||
(!status.north.isSet && status.west.isSet)) {
return true
} else if ((status.south.isSet && !status.east.isSet) ||
(!status.south.isSet && status.east.isSet)) {
return true;
}
return false;
},
/**
* Checks that there are either two or four coordinate values. If there aren't,
* it means that the user still needs to enter coordinates.
*
* @param status The current state of the coordinates
* @return {bool} True if there are pairs, false otherwise
*/
checkForPairs: function (status) {
var isSet = _.filter(status, function (coord) { return coord.isSet == true; });
if (isSet.length == 0) {
return false;
}
return true;
},
/**
* Validate a coordinate String by making sure it can be coerced into a number and
* is within the given bounds.
* Note: Min and max are inclusive
*
* @param value {string} The value of the edit area that will be validated
* @param min The minimum value that 'value' can be
* @param max The maximum value that 'value' can be
* @return {bool} True if the validation passed, otherwise false
*/
validateCoordinate: function (value, min, max) {
if (typeof value === "undefined" || value === null || value === "" && isNaN(value)) {
return false;
}
var parsed = Number(value);
if (isNaN(parsed)) {
return false;
}
if (parsed < min || parsed > max) {
return false;
}
return true;
},
/*
* 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;
},
trickleUpChange: function () {
this.get("parentModel").trigger("change");
this.get("parentModel").trigger("change:geoCoverage");
},
formatXML: function (xmlString) {
return DataONEObject.prototype.formatXML.call(this, xmlString);
}
});
return EMLGeoCoverage;
});