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

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

      /**
      * @class EMLParty
      * @classcategory Models/Metadata/EML211
      * @classdesc EMLParty represents a single Party from the EML 2.1.1 and 2.2.0
      * metadata schema. This can be a person or organization.
      * @see https://eml.ecoinformatics.org/schema/eml-party_xsd.html#ResponsibleParty
      * @extends Backbone.Model
      * @constructor
      */
  var EMLParty = Backbone.Model.extend(
    /** @lends EMLParty.prototype */{

    defaults: function(){
      return {
        objectXML: null,
        objectDOM: null,
        individualName: null,
        organizationName: null,
        positionName: null,
        address: [],
        phone: [],
        fax: [],
        email: [],
        onlineUrl: [],
        roles: [],
        references: null,
        userId: [],
        xmlID: null,
        type: null,
        typeOptions: ["associatedParty", "contact", "creator", "metadataProvider", "publisher"],
        roleOptions: ["custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator",
                      "coPrincipalInvestigator", "user"],
        parentModel: null,
        removed: false //Indicates whether this model has been removed from the parent model
      }
    },

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

      if(!this.get("xmlID"))
        this.createID();
      this.on("change:roles", this.setType);
    },

    /*
         * 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 {
        "administrativearea"    : "administrativeArea",
        "associatedparty"       : "associatedParty",
        "deliverypoint"         : "deliveryPoint",
        "electronicmailaddress" : "electronicMailAddress",
        "givenname"             : "givenName",
        "individualname"        : "individualName",
        "metadataprovider"      : "metadataProvider",
        "onlineurl"             : "onlineUrl",
        "organizationname"      : "organizationName",
        "positionname"          : "positionName",
        "postalcode"            : "postalCode",
        "surname"               : "surName",
        "userid"                : "userId"
      }
    },

        /*
            Parse the object DOM to create the model
            @param objectDOM the XML DOM to parse
            @return modelJSON the resulting model attributes object
         */
    parse: function(objectDOM){
      if(!objectDOM)
        var objectDOM = this.get("objectDOM");

      var model = this,
        modelJSON = {};

      //Set the name
      var person = $(objectDOM).children("individualname, individualName");

      if(person.length)
        modelJSON.individualName = this.parsePerson(person);

      //Set the phone and fax numbers
      var phones = $(objectDOM).children("phone"),
        phoneNums = [],
        faxNums = [];

      phones.each(function(i, phone){
        if($(phone).attr("phonetype") == "voice")
          phoneNums.push($(phone).text());
        else if($(phone).attr("phonetype") == "facsimile")
          faxNums.push($(phone).text());
      });

      modelJSON.phone = phoneNums;
      modelJSON.fax   = faxNums;

      //Set the address
      var addresses = $(objectDOM).children("address") || [],
        addressesJSON = [];

      addresses.each(function(i, address){
        addressesJSON.push(model.parseAddress(address));
      });

      modelJSON.address = addressesJSON;

      //Set the text fields
      modelJSON.organizationName = $(objectDOM).children("organizationname, organizationName").text() || null;
      modelJSON.positionName     = $(objectDOM).children("positionname, positionName").text() || null;
      // roles
      modelJSON.roles = [];
      $(objectDOM).find("role").each(function(i,role){
        modelJSON.roles.push($(role).text());
      });

      //Set the id attribute
      modelJSON.xmlID = $(objectDOM).attr("id");

      //Email - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
      if( $(objectDOM).children("electronicmailaddress, electronicMailAddress").length ){
        modelJSON.email = _.map($(objectDOM).children("electronicmailaddress, electronicMailAddress"), function(email){
                    return  $(email).text();
                  });
      }

      //Online URL - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
      if( $(objectDOM).find("onlineurl, onlineUrl").length ){
        // modelJSON.onlineUrl = [$(objectDOM).find("onlineurl, onlineUrl").first().text()];
        modelJSON.onlineUrl = $(objectDOM).find("onlineurl, onlineUrl").map(function(i,v) {
          return $(v).text();
        }).get();
      }

      //User ID - only set it on the JSON if it exists (we want to avoid an empty string value in the array)
      if( $(objectDOM).find("userid, userId").length ){
        modelJSON.userId = [$(objectDOM).find("userid, userId").first().text()];
      }

      return modelJSON;
    },

    parseNode: function(node){
      if(!node || (Array.isArray(node) && !node.length))
        return;

      this.set($(node)[0].localName, $(node).text());
    },

    parsePerson: function(personXML){
      var person = {
          givenName: [],
          surName: "",
          salutation: []
        },
        givenName   = $(personXML).find("givenname, givenName"),
        surName     = $(personXML).find("surname, surName"),
        salutations = $(personXML).find("salutation");

      //Concatenate all the given names into one, for now
      //TODO: Support multiple given names
      givenName.each(function(i, name){
        if(i==0)
          person.givenName[0] = "";

        person.givenName[0] += $(name).text() + " ";

        if(i==givenName.length-1)
          person.givenName[0] = person.givenName[0].trim();
      });

      person.surName = surName.text();

      salutations.each(function(i, name){
        person.salutation.push($(name).text());
      });

      return person;
    },

    parseAddress: function(addressXML){
      var address    = {},
        delPoint   = $(addressXML).find("deliverypoint, deliveryPoint"),
        city       = $(addressXML).find("city"),
        adminArea  = $(addressXML).find("administrativearea, administrativeArea"),
        postalCode = $(addressXML).find("postalcode, postalCode"),
        country    = $(addressXML).find("country");

      address.city               = city.length? city.text() : "";
      address.administrativeArea = adminArea.length? adminArea.text() : "";
      address.postalCode         = postalCode.length? postalCode.text() : "";
      address.country            = country.length? country.text() : "";

      //Get an array of all the address line (or delivery point) values
      var addressLines = [];
      _.each(delPoint, function(addressLine, i){
        addressLines.push($(addressLine).text());
      }, this);

      address.deliveryPoint = addressLines;

      return  address;
    },

    serialize: function(){
      var objectDOM = this.updateDOM(),
        xmlString = objectDOM.outerHTML;

      //Camel-case the XML
        xmlString = this.formatXML(xmlString);

        return xmlString;
    },

    /*
     * Updates the attributes on this model based on the application user (the app UserModel)
     */
    createFromUser: function(){
      //Create the name from the user
      var name = this.get("individualName") || {};
      name.givenName  = [MetacatUI.appUserModel.get("firstName")];
      name.surName    = MetacatUI.appUserModel.get("lastName");
      this.set("individualName", name);

      //Get the email and username
      if(MetacatUI.appUserModel.get("email"))
        this.set("email", [MetacatUI.appUserModel.get("email")]);

      this.set("userId", [MetacatUI.appUserModel.get("username")]);
    },

    /*
     * Makes a copy of the original XML DOM and updates it with the new values from the model.
     */
    updateDOM: function(){
      var type = this.get("type") || "associatedParty",
        objectDOM = this.get("objectDOM");

      // If there is already an XML node for this model and it is the wrong type,
      //   then replace the XML node contents
      if(objectDOM && objectDOM.nodeName != type.toUpperCase()){
        objectDOM = $(document.createElement(type)).html( objectDOM.innerHTML );
      }
      // If there is already an XML node for this model and it is the correct type,
      //   then simply clone the XML node
      else if(objectDOM){
        objectDOM = objectDOM.cloneNode(true);
      }
      // Otherwise, create a new XML node
      else{
        objectDOM = document.createElement(type);
      }

      //There needs to be at least one individual name, organization name, or position name
      if( this.nameIsEmpty() && !this.get("organizationName") && !this.get("positionName"))
        return "";

      var name = this.get("individualName");
      if(name){
        //Get the individualName node
        var nameNode = $(objectDOM).find("individualname");
        if(!nameNode.length){
          nameNode = document.createElement("individualname");
          $(objectDOM).prepend(nameNode);
        }

        //Empty the individualName node
        $(nameNode).empty();

         // salutation[s]
         if(!Array.isArray(name.salutation) && name.salutation)
          name.salutation = [name.salutation];

         _.each(name.salutation, function(salutation) {
           $(nameNode).prepend("<salutation>" + salutation + "</salutation>");
         });

         //Given name
         if(!Array.isArray(name.givenName) && name.givenName) name.givenName = [name.givenName];
         _.each(name.givenName, function(givenName) {

          //If there is a given name string, create a givenName node
          if(typeof givenName == "string" && givenName){
            $(nameNode).append("<givenname>" + givenName + "</givenname>");
          }

         });

         // surname
         if(name.surName)
           $(nameNode).append("<surname>" +  name.surName + "</surname>");
      }
      //If there is no name set on the model, remove it from the DOM
      else{
        $(objectDOM).find("individualname").remove();
      }

       // organizationName
      if(this.get("organizationName")){
        //Get the organization name node
        if($(objectDOM).find("organizationname").length)
          var orgNameNode = $(objectDOM).find("organizationname").detach();
        else
          var orgNameNode = document.createElement("organizationname");

        //Insert the text
        $(orgNameNode).text(this.get("organizationName"));

        //If the DOM is empty, append it
        if( !$(objectDOM).children().length )
          $(objectDOM).append(orgNameNode);
        else{
          var insertAfter = this.getEMLPosition(objectDOM, "organizationname");

          if(insertAfter && insertAfter.length)
            insertAfter.after(orgNameNode);
          else
            $(objectDOM).prepend(orgNameNode);
        }
      }
      //Remove the organization name node if there is no organization name
      else{
        var orgNameNode = $(objectDOM).find("organizationname").remove();
      }

       // positionName
      if(this.get("positionName")){
        //Get the name node
        if($(objectDOM).find("positionname").length)
          var posNameNode = $(objectDOM).find("positionname").detach();
        else
          var posNameNode = document.createElement("positionname");

        //Insert the text
        $(posNameNode).text(this.get("positionName"));

        //If the DOM is empty, append it
        if( !$(objectDOM).children().length )
          $(objectDOM).append(posNameNode);
        else{
          let insertAfter = this.getEMLPosition(objectDOM, "positionname")
          if(insertAfter)
            insertAfter.after(posNameNode);
          else
            $(objectDOM).prepend(posNameNode);
        }
      }
      //Remove the position name node if there is no position name
      else{
        $(objectDOM).find("positionname").remove();
      }

       // address
       _.each(this.get("address"), function(address, i) {

         var addressNode =  $(objectDOM).find("address")[i];

         if(!addressNode){
           addressNode = document.createElement("address");
           this.getEMLPosition(objectDOM, "address").after(addressNode);
         }

         //Remove all the delivery points since they'll be reserialized
         $(addressNode).find("deliverypoint").remove();

         _.each(address.deliveryPoint, function(deliveryPoint, ii){
           if(!deliveryPoint) return;

           var delPointNode = $(addressNode).find("deliverypoint")[ii];

           if(!delPointNode){
             delPointNode = document.createElement("deliverypoint");

             //Add the deliveryPoint node to the address node
             //Insert after the last deliveryPoint node
             var appendAfter = $(addressNode).find("deliverypoint")[ii-1];
             if(appendAfter)
               $(appendAfter).after(delPointNode);
             //Or just prepend to the beginning
             else
               $(addressNode).prepend(delPointNode);
           }

           $(delPointNode).text(deliveryPoint);
         });

         if(address.city){
           var cityNode = $(addressNode).find("city");

           if(!cityNode.length){
             cityNode = document.createElement("city");

             if(this.getEMLPosition(addressNode, "city")){
               this.getEMLPosition(addressNode, "city").after(cityNode);
             }
             else{
                $(addressNode).append(cityNode);
             }
           }

           $(cityNode).text(address.city);
         }
         else{
           $(addressNode).find("city").remove();
         }

         if(address.administrativeArea){
           var adminAreaNode = $(addressNode).find("administrativearea");

           if(!adminAreaNode.length){
             adminAreaNode = document.createElement("administrativearea");

             if(this.getEMLPosition(addressNode, "administrativearea")){
               this.getEMLPosition(addressNode, "administrativearea").after(adminAreaNode);
             }
             else{
                $(addressNode).append(adminAreaNode);
             }

           }

           $(adminAreaNode).text(address.administrativeArea);
         }
         else{
           $(addressNode).find("administrativearea").remove();
         }

         if(address.postalCode){
           var postalcodeNode = $(addressNode).find("postalcode");

           if(!postalcodeNode.length){
             postalcodeNode = document.createElement("postalcode");

             if(this.getEMLPosition(addressNode, "postalcode")){
               this.getEMLPosition(addressNode, "postalcode").after(postalcodeNode);
             }
             else{
                $(addressNode).append(postalcodeNode);
             }

           }

           $(postalcodeNode).text(address.postalCode);
         }
         else{
           $(addressNode).find("postalcode").remove();
         }

         if(address.country){
           var countryNode = $(addressNode).find("country");

           if(!countryNode.length){
             countryNode = document.createElement("country");

             if(this.getEMLPosition(addressNode, "country")){
               this.getEMLPosition(addressNode, "country").after(countryNode);
             }
             else{
                $(addressNode).append(countryNode);
             }

           }

           $(countryNode).text(address.country);
         }
         else{
           $(addressNode).find("country").remove();
         }

       }, this);

       if( this.get("address").length == 0 ){
         $(objectDOM).find("address").remove();
       }

       // phone[s]
       $(objectDOM).find("phone[phonetype='voice']").remove();
       _.each(this.get("phone"), function(phone) {

        var phoneNode = $(document.createElement("phone")).attr("phonetype", "voice").text(phone);
        this.getEMLPosition(objectDOM, "phone").after(phoneNode);

       }, this);

       // fax[es]
       $(objectDOM).find("phone[phonetype='facsimile']").remove();
       _.each(this.get("fax"), function(fax) {

        var faxNode = $(document.createElement("phone")).attr("phonetype", "facsimile").text(fax);
        this.getEMLPosition(objectDOM, "phone").after(faxNode);

       }, this);

       // electronicMailAddress[es]
       $(objectDOM).find("electronicmailaddress").remove();
       _.each(this.get("email"), function(email) {

         var emailNode = document.createElement("electronicmailaddress");
         this.getEMLPosition(objectDOM, "electronicmailaddress").after(emailNode);

         $(emailNode).text(email);

       }, this);

      // online URL[es]
      $(objectDOM).find("onlineurl").remove();
       _.each(this.get("onlineUrl"), function(onlineUrl, i) {

        var urlNode = document.createElement("onlineurl");
        this.getEMLPosition(objectDOM, "onlineurl").after(urlNode);

        $(urlNode).text(onlineUrl);

       }, this);

       //user ID
       var userId = Array.isArray(this.get("userId")) ? this.get("userId") : [this.get("userId")];
       _.each(userId, function(id) {
         if(!id) return;

         var idNode = $(objectDOM).find("userid");

         //Create the userid node
         if(!idNode.length){

           idNode = $(document.createElement("userid"));

           this.getEMLPosition(objectDOM, "userid").after(idNode);
         }

         //If this is an orcid identifier, format it correctly
         if(this.isOrcid(id)){
            // Add the directory attribute
           idNode.attr("directory", "https://orcid.org");

           //If this ORCID does not start with "http"
           if( id.indexOf("http") == -1 ){
             //If this is an ORCID with just the 16-digit numbers and hyphens, then add
             // the https://orcid.org/ prefix to it
             if( id.length == 19){
               id = "https://orcid.org/" + id;
             }
             //If it starts with "orcid.org", then add the "https://" prefix
             else if( id.indexOf("orcid.org") == 0 ){
               id = "https://" + id;
             }
             //If it starts with "www.orcid.org", then add the "https" prefix and remove the "www"
             else if( id.indexOf("www.orcid.org") == 0 ){
               id = "https://" + id.replace("www.orcid.org", "orcid.org");
             }
           }

           //If there is a "www", remove it
           if( id.indexOf("www.orcid.org") > -1 ){
             id = id.replace("www.orcid.org", "orcid.org");
           }

           //If it has the http:// prefix, add the 's' for secure protocol
           if( id.indexOf("http://") == 0){
             id = id.replace("http", "https");
           }

         }
         else{
           idNode.attr("directory", "unknown");
         }

         $(idNode).text(id);

       }, this);

       //Remove all the user id's if there aren't any in the model
       if( userId.length == 0 ){
         $(objectDOM).find("userid").remove();
       }

      // role
      //If this party type is not an associated party, then remove the role element
      if( type != "associatedParty" && type != "personnel" ){
        $(objectDOM).find("role").remove();
      }
      //Otherwise, change the value of the role element
      else {
        // If for some reason there is no role, create a default role
        if( !this.get("roles").length ){
          var roles = ["Associated Party"];
        } else {
          var roles = this.get("roles");
        }
        _.each(roles, function(role, i){
          var roleSerialized = $(objectDOM).find("role");
          if(roleSerialized.length){
            $(roleSerialized[i]).text(role)
          } else {
            roleSerialized = $(document.createElement("role")).text(role);
            this.getEMLPosition(objectDOM, "role").after( roleSerialized );
          }
        }, this);

      }

      //XML id attribute
      this.createID();
      //if(this.get("xmlID"))
        $(objectDOM).attr("id", this.get("xmlID"));
      //else
      //  $(objectDOM).removeAttr("id");

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

       return objectDOM;
    },

      /*
      * Adds this EMLParty model to it's parent EML211 model in the appropriate role array
      *
      * @return {boolean} - Returns true if the merge was successful, false if the merge was cancelled
      */
      mergeIntoParent: function(){
        //Get the type of EML Party, in relation to the parent model
        if(this.get("type") && this.get("type") != "associatedParty")
          var type = this.get("type");
        else
          var type = "associatedParty";

        //Update the list of EMLParty models in the parent model
        var parentEML = this.getParentEML();

        if(parentEML.type != "EML")
          return false;

        //Add this model to the EML model
        var successfulAdd = parentEML.addParty(this);

        //Validate the model
        this.isValid();

        return successfulAdd;
      },

      isEmpty: function(){
        // If we add any new fields, be sure to add the attribute here
        var attributes = ["userId", "fax", "phone", "onlineUrl",
                          "email", "positionName", "organizationName"];

         //Check each value in the model that gets serialized to see if there is a value
         for(var i in attributes) {
           //Get the value from the model for this attribute
            var modelValue = this.get(attributes[i]);

            //If this is an array, then we want to check if there are any values in it
            if( Array.isArray(modelValue) ){
             if( modelValue.length > 0 )
                return false;
            }
            //Otherwise, check if this value differs from the default value
            else if(this.get(attributes[i]) !== this.defaults()[attributes[i]]){
              return false;
            }
         }

         //Check for a first and last name
         if( this.get("individualName") && (this.get("individualName").givenName || this.get("individualName").surName) )
          return false;

          //Check for addresses
          var isAddress = false;

          if( this.get("address") ){

            //Checks if there are any values anywhere in the address
            _.each(this.get("address"), function(address){
              //Delivery point is an array so we need to check the first and second
              //values of that array
              if( address.administrativeArea  || address.city ||
                  address.country || address.postalCode ||
                  (address.deliveryPoint && address.deliveryPoint.length &&
                  (address.deliveryPoint[0] || address.deliveryPoint[1]) ) ){
                isAddress = true;
              }
            });

          }

          //If we found an address value anywhere, then it is not empty
          if(isAddress)
            return false;

         //If we never found a value, then return true because this model is empty
         return true;
      },

    /*
         * Returns the node in the given EML snippet that the given node type should be inserted after
         */
        getEMLPosition: function(objectDOM, nodeName){
          var nodeOrder = [ "individualname", "organizationname", "positionname", "address", "phone",
                            "electronicmailaddress", "onlineurl", "userid", "role"];
          var addressOrder = ["deliverypoint", "city", "administrativearea", "postalcode", "country"];

          //If this is an address node, find the position within the address
          if( _.contains(addressOrder, nodeName) ){
            nodeOrder = addressOrder;
          }

          var position = _.indexOf(nodeOrder, nodeName);
          if(position == -1)
            return $(objectDOM).children().last();

          //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();
          }

          return false;
        },

    createID: function(){
      this.set("xmlID", Math.ceil(Math.random() * (9999999999999999 - 1000000000000000) + 1000000000000000));
    },

    setType: function(){
      if(this.get("roles")){
        if(this.get("roles").length && !this.get("type")){
          this.set("type", "associatedParty");
        }
      }
    },

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

    removeFromParent: function(){
        if( !this.get("parentModel") )
            return;
        else if( typeof this.get("parentModel").removeParty != "function" )
            return;

        this.get("parentModel").removeParty(this);

        this.set("removed", true);
    },

    /*
     * Checks the values of the model to determine if it is EML-valid
     */
    validate: function(){

      var individualName = this.get("individualName") || {},
        givenName = individualName.givenName || [],
        surName   = individualName.surName || null,
    errors = {};

      //If there are no values in this model that would be serialized, then the model is valid
      if( !this.get("organizationName") && !this.get("positionName") && !givenName[0]?.length && !surName
          && !this.get("address").length && !this.get("phone").length && !this.get("fax").length
          && !this.get("email").length && !this.get("onlineUrl").length && !this.get("userId").length){

        return;

      }

      //The EMLParty must have either an organization name, position name, or surname.
      // It must ALSO have a type or role.
      if ( !this.get("organizationName") && !this.get("positionName") &&
          (!this.get("individualName") || !surName ) ){

        errors = {
          surName: "Either a last name, position name, or organization name is required.",
          positionName: "",
          organizationName: ""
        }

      }
      //If there is a first name and no last name, then this is not a valid individualName
      else if( (givenName[0]?.length && !surName) && this.get("organizationName") && this.get("positionName") ){

        errors = { surName: "Provide a last name." }

      }

      //Check that each required field has a value. Required fields are configured in the {@link AppConfig}
      let roles = this.get("type") == "associatedParty"? this.get("roles") : [this.get("type")];
      for( role of roles ){
        let requiredFields = MetacatUI.appModel.get("emlEditorRequiredFields_EMLParty")[role];
        requiredFields?.forEach(field=>{
          let currentVal = this.get(field);
          if( !currentVal || !currentVal?.length ){
            errors[field] = `Provide a${(["a","e","i","o","u"].includes(field.charAt(0))? "n " : " ")} ${field}. `;
          }
        });
      }

	
    return Object.keys(errors)?.length? errors : false;

  
    },

    isOrcid: function(username){
      if(!username) return false;

      //If the ORCID URL is anywhere in this username string, it is an ORCID
      if(username.indexOf("orcid.org") > -1){
        return true;
      }

      /* The ORCID checksum algorithm to determine is a character string is an ORCiD
       * http://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
       */
      var total = 0,
          baseDigits = username.replace(/-/g, "").substr(0, 15);

      for(var i=0; i<baseDigits.length; i++){
        var digit = parseInt(baseDigits.charAt(i));
        total = (total + digit) * 2;
      }

      var remainder = total % 11,
        result = (12 - remainder) % 11,
        checkDigit = (result == 10) ? "X" : result.toString(),
        isOrcid = (checkDigit == username.charAt(username.length-1));

      return isOrcid;
    },

    /*
    *  Clones all the values of this array into a new JS Object.
    *  Special care is needed for nested objects and arrays
    *  This is helpful when copying this EMLParty to another role in the EML
    */
    copyValues: function(){
      //Get a JSON object of all the model copyValues
      var modelValues = this.toJSON();

      //Go through each model value and properly clone the arrays
      _.each( Object.keys(modelValues), function(key, i){

        //Clone the array via slice()
        if( Array.isArray(modelValues[key]) )
          modelValues[key] = modelValues[key].slice(0);

      });

      //Individual Names are objects, so properly clone them
      if( modelValues.individualName ){
        modelValues.individualName = Object.assign({}, modelValues.individualName);
      }

      //Addresses are objects, so properly clone them
      if( modelValues.address.length ){
        _.each(modelValues.address, function(address, i){
          modelValues.address[i] = Object.assign({}, address);

          //The delivery point is an array of strings, so properly clone the array
          if( Array.isArray(modelValues.address[i].deliveryPoint) )
            modelValues.address[i].deliveryPoint = modelValues.address[i].deliveryPoint.slice(0);
        });
      }
      return modelValues;
  },

  /**
   * getName - For an individual, returns the first and last name as a string. Otherwise,
   * returns the organization or position name.
   *
   * @return {string} Returns the name of the party or an empty string if one cannot be found
   *
   * @since 2.15.0
   */
  getName: function(){
    return this.get("individualName") ?
      this.get("individualName").givenName + " " + this.get("individualName").surName :
      this.get("organizationName") || this.get("positionName") || "";
      },
      
    /**
     * Return the EML Party as a CSL JSON object. See
     * {@link https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#names}.
     * @return {object} The CSL JSON object
     * @since 2.23.0
     */
    toCSLJSON: function () {
      const name = this.get("individualName");
      const csl= {
        family: name?.surName || null,
        given: name?.givenName || null,
        literal: this.get("organizationName") || this.get("positionName") || "",
        "dropping-particle": name?.salutation || null,
      }
      // If any of the fields are arrays, join them with a space
      for (const key in csl) {
        if (Array.isArray(csl[key])) {
          csl[key] = csl[key].join(" ");
        }
      }
      return csl;
    },

    /*
    * function nameIsEmpty - Returns true if the individualName set on this
    * model contains only empty values. Otherwise, returns false. This is just a
    * shortcut for manually checking each name field individually.
    *
    * @return {boolean}
    */
    nameIsEmpty: function(){
      var name = this.get("individualName");

      if( !name || typeof name != "object" )
        return true;

      //Check if there are given names
      var givenName = name.givenName,
          givenNameEmpty = false;

      if( !givenName || (Array.isArray(givenName) && givenName.length == 0) ||
        (typeof givenName == "string" && givenName.trim().length == 0) )
          givenNameEmpty = true;

      //Check if there are no sur names
      var surName = name.surName,
          surNameEmpty = false;

      if( !surName || (Array.isArray(surName) && surName.length == 0) ||
        (typeof surName == "string" && surName.trim().length == 0) )
          surNameEmpty = true;

      //Check if there are no salutations
      var salutation = name.salutation,
          salutationEmpty = false;

      if( !salutation || (Array.isArray(salutation) && salutation.length == 0) ||
        (typeof salutation == "string" && salutation.trim().length == 0) )
          salutationEmpty = true;

      if( givenNameEmpty && surNameEmpty && salutationEmpty )
        return true;
      else
        return false;

    },

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

    },

  /**
   * @type {object[]}
   * @property {string} label - The name of the party category to display to the user
   * @property {string} dataCategory - The string that is used to represent this party. This value
   *  should exactly match one of the strings listed in EMLParty typeOptions or EMLParty roleOptions.
   * @property {string} description - An optional description to display below the label to help the user
   *  with this category.
   * @property {boolean} createFromUser - If set to true, the information from the logged-in user will be
   *  used to create an EML party for this category if none exist already when the view loads.
   * @property {number} limit - If the number of parties allowed for this category is not unlimited,
   *  then limit should be set to the maximum allowable number.
   * @since 2.21.0
   */
   partyTypes: [
    {
      label: "Dataset Creators (Authors/Owners/Originators)",
      dataCategory: "creator",
      description: "Each person or organization listed as a Creator will be listed in the data" +
      " citation. At least one person, organization, or position with a 'Creator'" +
      " role is required.",
      createFromUser: true
    },
    {
      label: "Contact",
      dataCategory: "contact",
      createFromUser: true
    },
    {
      label: "Principal Investigators",
      dataCategory: "principalInvestigator"
    },
    {
      label: "Co-Principal Investigators",
      dataCategory: "coPrincipalInvestigator"
    },
    {
      label: "Collaborating-Principal Investigators",
      dataCategory: "collaboratingPrincipalInvestigator"
    },
    {
      label: "Metadata Provider",
      dataCategory: "metadataProvider"
    },
    {
      label: "Custodians/Stewards",
      dataCategory: "custodianSteward"
    },
    {
      label: "Publisher",
      dataCategory: "publisher",
      description: "Only one publisher can be specified.",
      limit: 1
    },
    {
      label: "Users",
      dataCategory: "user"
    }
    ],

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

  return EMLParty;
});