Source: src/js/models/metadata/eml211/EMLTemporalCoverage.js

/* global define */
define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'],
    function($, _, Backbone, DataONEObject) {

  /**
  * @class EMLTemporalCoverage
  * @classcategory Models/Metadata/EML211
  */
	var EMLTemporalCoverage = Backbone.Model.extend(
    /** @lends EMLTemporalCoverage.prototype */{

		defaults: {
			objectXML: null,
			objectDOM: null,
			beginDate: null,
			beginTime: null,
			endDate: null,
			endTime: null
		},

		initialize: function(attributes){
			if(attributes && attributes.objectDOM)
				this.set(this.parse(attributes.objectDOM));

			this.on("change:beginDate change:beginTime change:endDate change:endTime", 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 {
        		"begindate" : "beginDate",
        		"calendardate" : "calendarDate",
        		"enddate" : "endDate",
            	"rangeofdates" : "rangeOfDates",
            	"singledatetime" : "singleDateTime",
            	"spatialraster" : "spatialRaster",
            	"spatialvector" : "spatialVector",
            	"storedprocedure" : "storedProcedure",
            	"temporalcoverage" : "temporalCoverage"
            }
        },

		parse: function(objectDOM){
			if(!objectDOM) var objectDOM = this.get("objectDOM");

			var rangeOfDates   = $(objectDOM).children('rangeofdates'),
				singleDateTime = $(objectDOM).children('singledatetime');

			// If the temporalCoverage element has both a rangeOfDates and a
			// singleDateTime (invalid EML), the rangeOfDates is preferred.
			if (rangeOfDates.length) {
				return this.parseRangeOfDates(rangeOfDates);
			} else if (singleDateTime.length) {
				return this.parseSingleDateTime(singleDateTime);
			}
		},

		parseRangeOfDates: function(rangeOfDates) {
			var beginDate = $(rangeOfDates).find('beginDate'),
				endDate = $(rangeOfDates).find('endDate'),
				properties = {};

			if (beginDate.length > 0) {
				if ($(beginDate).find('calendardate')) {
					properties.beginDate = $(beginDate).find('calendardate').first().text();
				}

				if ($(beginDate).find('time').length > 0) {
					properties.beginTime = $(beginDate).find('time').first().text();
				}
			}

			if (endDate.length > 0) {
				if ($(endDate).find('calendardate').length > 0) {
					properties.endDate = $(endDate).find('calendardate').first().text();
				}

				if ($(endDate).find('time').length > 0) {
					properties.endTime = $(endDate).find('time').first().text();
				}
			}

			return properties;
		},

		parseSingleDateTime: function(singleDateTime) {
			var calendarDate = $(singleDateTime).find("calendardate"),
			    time = $(singleDateTime).find("time");

			return {
				beginDate: calendarDate.length > 0 ? calendarDate.first().text() : null,
				beginTime: time.length > 0 ? time.first().text() : null
			};
		},

		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.get("objectDOM")) {
				objectDOM = this.get("objectDOM").cloneNode(true);
				//Empty the DOM
				$(objectDOM).empty();
			} else {
				objectDOM = $("<temporalcoverage></temporalcoverage>");
			}

			if (this.get('beginDate') && this.get('endDate')) {
				$(objectDOM).append(this.serializeRangeOfDates());
			} else if (!this.get('endDate')) {
				$(objectDOM).append(this.serializeSingleDateTime());
			}
			else if(this.get("singleDateTime")){
				var singleDateTime = $(objectDOM).find("singledatetime");
				if(!singleDateTime.length){
					singleDateTime = document.createElement("singledatetime");
					$(objectDOM).append(singleDateTime);
				}

				if(this.get("singleDateTime").calendarDate)
					$(singleDateTime).html(this.serializeSingleDateTime( this.get("singleDateTime").calendarDate ));
			}

			// Remove empty (zero-length or whitespace-only) nodes
			$(objectDOM).find("*").filter(function() { return $.trim(this.innerHTML) === ""; } ).remove();

			return objectDOM;
		},

		serializeRangeOfDates: function() {
			var objectDOM = $(document.createElement('rangeofdates')),
			    beginDateEl = $(document.createElement('begindate')),
				endDateEl = $(document.createElement('enddate'));

			if (this.get('beginDate')) {
				$(beginDateEl).append(this.serializeCalendarDate(this.get('beginDate')));

				if (this.get('beginTime')) {
					$(beginDateEl).append(this.serializeTime(this.get('beginTime')));
				}

				objectDOM.append(beginDateEl);
			}

			if (this.get('endDate')) {
				$(endDateEl).append(this.serializeCalendarDate(this.get('endDate')));

				if (this.get('endTime')) {
					$(endDateEl).append(this.serializeTime(this.get('endTime')));
				}
				objectDOM.append(endDateEl);
			}

			return objectDOM;
		},

		serializeSingleDateTime: function() {
			var objectDOM = $(document.createElement('singleDateTime'));

			if (this.get('beginDate')) {
				$(objectDOM).append(this.serializeCalendarDate(this.get('beginDate')));

				if (this.get('beginTime')) {
					$(objectDOM).append(this.serializeTime(this.get('beginTime')));
				}
			}

			return objectDOM;
		},

		serializeCalendarDate: function(date) {
			return $(document.createElement('calendarDate')).html(date);
		},

		serializeTime: function(time) {
			return $(document.createElement('time')).html(time);
		},

		trickleUpChange: function(){
			if(_.contains(MetacatUI.rootDataPackage.models, this.get("parentModel")))
				MetacatUI.rootDataPackage.packageModel.set("changed", true);
		},

		mergeIntoParent: function(){
			if(this.get("parentModel") && this.get("parentModel").type == "EML" && !_.contains(this.get("parentModel").get("temporalCoverage"), this))
				this.get("parentModel").get("temporalCoverage").push(this);
		},

		formatXML: function(xmlString){
			return DataONEObject.prototype.formatXML.call(this, xmlString);
		},

		// Checks the values of this model and determines whether they are valid according the the EML 2.1.1 schema.
		// Returns a hash of error messages
		validate: function() {
			var beginDate = this.get('beginDate'),
			    beginTime = this.get('beginTime'),
				endDate = this.get('endDate'),
				endTime = this.get('endTime'),
				errors  = {};

			// A valid temporal coverage at least needs a start date
			if (!beginDate) {
				errors.beginDate = "Provide a begin date.";
			}
			// endTime is set but endDate is not
			else if (endTime && endTime.length > 0 && (!endDate || endDate.length == 0)) {
				errors.endDate = "Provide an end date."
			}

			//Check the validity of the date format
			if(beginDate && !this.isDateFormatValid(beginDate)){
				errors.beginDate = "The begin date must be formatted as YYYY-MM-DD or YYYY.";
			}

			//Check the validity of the date format
			if(endDate && !this.isDateFormatValid(endDate)){
				errors.endDate = "The end date must be formatted as YYYY-MM-DD or YYYY.";
			}

      if( typeof endDate == "string" && endDate.length && beginDate <= 0 ){
        errors.beginDate = "The begin date must be greater than zero.";
      }

      if( typeof endDate == "string" && endDate.length && endDate <= 0 ){
        errors.endDate = "The end date must be greater than zero.";
      }

			//Check the validity of the begin time format
			if(beginTime){
				var timeErrorMessage = this.validateTimeFormat(beginTime);

				if( typeof timeErrorMessage == "string" )
					errors.beginTime = timeErrorMessage;
			}

			//Check the validity of the end time format
			if(endTime){
				var timeErrorMessage = this.validateTimeFormat(endTime);

				if( typeof timeErrorMessage == "string" )
					errors.endTime = timeErrorMessage;
			}

			// Check if begin date greater than end date for the temporalCoverage
			if (this.isGreaterDate(beginDate, endDate))
				errors.beginDate = "The begin date must be before the end date."

			// Check if begin time greater than end time for the temporalCoverage in case of equal dates.
			if (this.isGreaterTime(beginDate, endDate, beginTime, endTime))
				errors.beginTime = "The begin time must be before the end time."

			if(Object.keys(errors).length)
				return errors;
			else
				return;
		},

		isDateFormatValid: function(dateString){

			//Date strings that are four characters should be a full year. Make sure all characters are numbers
			if(dateString.length == 4){
				var digits = dateString.match( /[0-9]/g );
				return (digits.length == 4)
			}
			//Date strings that are 10 characters long should be a valid date
			else{
				var dateParts = dateString.split("-");

				if(dateParts.length != 3 || dateParts[0].length != 4 || dateParts[1].length != 2 || dateParts[2].length != 2)
					return false;

				dateYear = dateParts[0];
				dateMonth = dateParts[1];
				dateDay = dateParts[2];

				// Validating the values for the date and month if in YYYY-MM-DD format.
				if (dateMonth < 1 || dateMonth > 12)
					return false;
				else if (dateDay < 1 || dateDay > 31)
					return false;
				else if ((dateMonth == 4 || dateMonth == 6 || dateMonth == 9 || dateMonth == 11) && dateDay == 31)
					return false;
				else if (dateMonth == 2) {
				// Validation for leap year dates.
					var isleap = (dateYear % 4 == 0 && (dateYear % 100 != 0 || dateYear % 400 == 0));
					if ((dateDay > 29) || (dateDay == 29 && !isleap))
						return false;
				}

				var digits = _.filter(dateParts, function(part){
					return (part.match( /[0-9]/g ).length == part.length);
				});

				return (digits.length == 3);
			}
		},

		validateTimeFormat: function(timeString){

			//If the last character is a "Z", then remove it for now
			if( timeString.substring(timeString.length-1, timeString.length) == "Z"){
				timeString = timeString.replace("Z", "", "g");
			}

			if(timeString.length == 8){
				var timeParts = timeString.split(":");

				if(timeParts.length != 3){
					return "Time must be formatted as HH:MM:SS";
				}

				// Validation pattern for HH:MM:SS values.
				// Range for HH validation : 00-24
				// Range for MM validation : 00-59
				// Range for SS validation : 00-59
				// Leading 0's are must in case of single digit values.
				var timePattern = /^(?:2[0-4]|[01][0-9]):[0-5][0-9]:[0-5][0-9]$/,
					validTimePattern = timeString.match(timePattern);

				//If the hour is 24, only accept 00:00 for MM:SS. Any minutes or seconds in the midnight hour should be
				//formatted as 00:XX:XX not 24:XX:XX
				if(validTimePattern && timeParts[0] == "24" && (timeParts[1] != "00" || timeParts[2] != "00")){
					return "The midnight hour starts at 00:00:00 and ends at 00:59:59.";
				}
				else if(!validTimePattern && parseInt(timeParts[0]) > "24"){
					return "Time of the day starts at 00:00 and ends at 23:59.";
				}
				else if(!validTimePattern && parseInt(timeParts[1]) > "59"){
					return "Minutes should be between 00 and 59.";
				}
				else if(!validTimePattern && parseInt(timeParts[2]) > "59"){
					return "Seconds should be between 00 and 59.";
				}
				else
					return true;

			}
			else
				return "Time must be formatted as HH:MM:SS";
		},

		/**
		 * This function checks whether the begin date is greater than the end date.
		 *
		 * @param {string} beginDate the begin date string
		 * @param {string} endDate the end date string
		 * @return {boolean}
		 */
		isGreaterDate: function(beginDate, endDate) {

			if(typeof beginDate == "undefined" || !beginDate)
				return false;

			if(typeof endDate == "undefined" || !endDate)
				return false;

			//Making sure that beginDate year is smaller than endDate year
			if (beginDate.length == 4 && endDate.length == 4) {
				if (beginDate > endDate) {
					return true;
				}
			}

			//Checking equality for either dateStrings that are greater than 4 characters
			else {
				beginDateParts = beginDate.split("-");
				endDateParts = endDate.split("-");

				if (beginDateParts.length == endDateParts.length) {
					if (beginDateParts[0] > endDateParts[0]) {
						return true;
					}
					else if (beginDateParts[0] == endDateParts[0]) {
						if (beginDateParts[1] > endDateParts[1]) {
							return true;
						}
						else if (beginDateParts[1] == endDateParts[1]) {
							if (beginDateParts[2] > endDateParts[2]) {
								return true;
							}
						}
					}
				}
				else {
					if (beginDateParts[0] > endDateParts[0]) {
						return true;
					}
				}
			}
			return false;
		},

        /**
		 * This function checks whether the begin time is greater than the end time.
		 *
		 * @param {string} beginDate the begin date string
		 * @param {string} endDate the end date string
		 * @param {string} beginTime the begin time string
		 * @param {string} endTime the end time string
		 * @return {boolean}
		 */
		isGreaterTime: function (beginDate, endDate, beginTime, endTime) {
			if(!beginTime || !endTime)
				return false;

			var equalDates = false;

			//Making sure that beginDate year is smaller than endDate year
			if (beginDate.length == 4 && endDate.length == 4) {
				if (beginDate == endDate) {
					equalDates = true;
				}
			}

			else {
				beginDateParts = beginDate.split("-");
				endDateParts = endDate.split("-");

				if (beginDateParts.length == endDateParts.length) {
					if (beginDateParts[0] == endDateParts[0]) {
						if (beginDateParts[1] == endDateParts[1]) {
							if (beginDateParts[2] == endDateParts[2]) {
								equalDates = true;
							}
						}
					}
				}
			}

			// If the dates are equal, check for validity of time frame.
			if (equalDates) {
				beginTimeParts = beginTime.split(":");
				endTimeParts = endTime.split(":");
				if (beginTimeParts[0] > endTimeParts[0]) {
					return true;
				}
				else if (beginTimeParts[0] == endTimeParts[0]) {
					if (beginTimeParts[1] > endTimeParts[1]) {
						return true;
					}
					else if (beginTimeParts[1] == endTimeParts[1]) {
						if (beginTimeParts[2] > endTimeParts[2]) {
							return true;
						}
					}
				}
			}
			return false;
		},

    /**
    * Checks if this model has no values set on it
    * @return {boolean}
    */
    isEmpty: function(){

      return (!this.get('beginDate') && !this.get('beginTime') && !this.get('endDate')
              && !this.get('endTime'));

    },

    /*
    * 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;

    }
	});

	return EMLTemporalCoverage;
});