# Source: src/js/models/metadata/eml211/EML211.js

    /* global define */
    define(['jquery', 'underscore', 'backbone', 'uuid',
            'collections/Units',
            'models/metadata/ScienceMetadata',
            'models/DataONEObject',
            'models/metadata/eml211/EMLGeoCoverage',
            'models/metadata/eml211/EMLKeywordSet',
            'models/metadata/eml211/EMLTaxonCoverage',
            'models/metadata/eml211/EMLTemporalCoverage',
            'models/metadata/eml211/EMLDistribution',
            'models/metadata/eml211/EMLEntity',
            'models/metadata/eml211/EMLDataTable',
            'models/metadata/eml211/EMLOtherEntity',
            'models/metadata/eml211/EMLParty',
            'models/metadata/eml211/EMLProject',
            'models/metadata/eml211/EMLText',
            'models/metadata/eml211/EMLMethods',
            'collections/metadata/eml/EMLAnnotations',
            'models/metadata/eml211/EMLAnnotation'],
        function($, _, Backbone, uuid, Units, ScienceMetadata, DataONEObject,
            EMLGeoCoverage, EMLKeywordSet, EMLTaxonCoverage, EMLTemporalCoverage,
            EMLDistribution, EMLEntity, EMLDataTable, EMLOtherEntity, EMLParty,
                EMLProject, EMLText, EMLMethods, EMLAnnotations, EMLAnnotation) {

          /**
          * @class EML211
          * @classdesc An EML211 object represents an Ecological Metadata Language
          * document, version 2.1.1
          * @classcategory Models/Metadata/EML211
          * @extends ScienceMetadata
          */
          var EML211 = ScienceMetadata.extend(
            /** @lends EML211.prototype */{

            type: "EML",

            defaults: function(){
              return _.extend(ScienceMetadata.prototype.defaults(), {
                id: "urn:uuid:" + uuid.v4(),
                formatId: "https://eml.ecoinformatics.org/eml-2.2.0",
                objectXML: null,
                  isEditable: false,
                  alternateIdentifier: [],
                  shortName: null,
                  title: [],
                  creator: [], // array of EMLParty objects
                  metadataProvider: [], // array of EMLParty objects
                  associatedParty : [], // array of EMLParty objects
                  contact: [], // array of EMLParty objects
                  publisher: [], // array of EMLParty objects
                  pubDate: null,
                  language: null,
                  series: null,
                  abstract: [], //array of EMLText objects
                  keywordSets: [], //array of EMLKeywordSet objects
                  additionalInfo: [],
                  intellectualRights: "This work is dedicated to the public domain under the Creative Commons Universal 1.0 Public Domain Dedication. To view a copy of this dedication, visit https://creativecommons.org/publicdomain/zero/1.0/.",
                  onlineDist: [], // array of EMLOnlineDist objects
                  offlineDist: [], // array of EMLOfflineDist objects
                  geoCoverage : [], //an array for EMLGeoCoverages
                  temporalCoverage : [], //an array of EMLTempCoverage models
                  taxonCoverage : [], //an array of EMLTaxonCoverages
                  purpose: [],
                  entities: [], //An array of EMLEntities
                  pubplace: null,
                  methods: new EMLMethods(), // An EMLMethods objects
                  project: null, // An EMLProject object,
                  annotations: null, // Dataset-level annotations
                  dataSensitivityPropertyURI: "http://purl.dataone.org/odo/SENSO_00000005",
                  nodeOrder: [
                    "alternateidentifier",
                    "shortname",
                    "title",
                    "creator",
                    "metadataprovider",
                    "associatedparty",
                    "pubdate",
                    "language",
                    "series",
                    "abstract",
                    "keywordset",
                    "additionalinfo",
                    "intellectualrights",
                    "licensed",
                    "distribution",
                    "coverage",
                    "annotation",
                    "purpose",
                    "introduction",
                    "gettingstarted",
                    "acknowledgements",
                    "maintenance",
                    "contact",
                    "publisher",
                    "pubplace",
                    "methods",
                    "project",
                    "datatable",
                    "spatialraster",
                    "spatialvector",
                    "storedprocedure",
                    "view",
                    "otherentity",
                    "referencepublications",
                    "usagecitations",
                    "literaturecited",
                  ]
              });
            },

            units: new Units(),

            initialize: function(attributes) {
                // Call initialize for the super class
                ScienceMetadata.prototype.initialize.call(this, attributes);

                // EML211-specific init goes here
                // this.set("objectXML", this.createXML());
                this.parse(this.createXML());

                this.on("sync", function(){
                  this.set("synced", true);
                });

                //Create a Unit collection
                if(!this.units.length)
                  this.createUnits();
            },

            url: function(options) {
                var identifier;
                if ( options && options.update ) {
                    identifier = this.get("oldPid") || this.get("seriesid");
                } else {
                    identifier = this.get("id") || this.get("seriesid");
                }
                return MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(identifier);
            },

            /*
             * 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 _.extend(
                  this.constructor.__super__.nodeNameMap(),
                  EMLDistribution.prototype.nodeNameMap(),
                  EMLGeoCoverage.prototype.nodeNameMap(),
                  EMLKeywordSet.prototype.nodeNameMap(),
                  EMLParty.prototype.nodeNameMap(),
                  EMLProject.prototype.nodeNameMap(),
                  EMLTaxonCoverage.prototype.nodeNameMap(),
                  EMLTemporalCoverage.prototype.nodeNameMap(),
                  EMLMethods.prototype.nodeNameMap(),
                  {
                    "accuracyreport" : "accuracyReport",
                    "actionlist" : "actionList",
                    "additionalclassifications" : "additionalClassifications",
                    "additionalinfo" : "additionalInfo",
                    "additionallinks" : "additionalLinks",
                    "additionalmetadata" : "additionalMetadata",
                    "allowfirst" : "allowFirst",
                    "alternateidentifier" : "alternateIdentifier",
                    "altitudedatumname" : "altitudeDatumName",
                    "altitudedistanceunits" : "altitudeDistanceUnits",
                    "altituderesolution" : "altitudeResolution",
                    "altitudeencodingmethod" : "altitudeEncodingMethod",
                    "altitudesysdef" : "altitudeSysDef",
                    "asneeded" : "asNeeded",
                    "associatedparty" : "associatedParty",
                    "attributeaccuracyexplanation" : "attributeAccuracyExplanation",
                    "attributeaccuracyreport" : "attributeAccuracyReport",
                    "attributeaccuracyvalue" : "attributeAccuracyValue",
                    "attributedefinition" : "attributeDefinition",
                    "attributelabel" : "attributeLabel",
                    "attributelist" : "attributeList",
                    "attributename" : "attributeName",
                    "attributeorientation" : "attributeOrientation",
                    "attributereference" : "attributeReference",
                    "awardnumber" : "awardNumber",
                    "awardurl" : "awardUrl",
                    "audiovisual" : "audioVisual",
                    "authsystem" : "authSystem",
                    "banddescription" : "bandDescription",
                    "bilinearfit" : "bilinearFit",
                    "binaryrasterformat" : "binaryRasterFormat",
                    "blockedmembernode" : "blockedMemberNode",
                    "booktitle" : "bookTitle",
                    "cameracalibrationinformationavailability" : "cameraCalibrationInformationAvailability",
                    "casesensitive" : "caseSensitive",
                    "cellgeometry" : "cellGeometry",
                    "cellsizexdirection" : "cellSizeXDirection",
                    "cellsizeydirection" : "cellSizeYDirection",
                    "changehistory" : "changeHistory",
                    "changedate" : "changeDate",
                    "changescope" : "changeScope",
                    "chapternumber" : "chapterNumber",
                    "characterencoding" : "characterEncoding",
                    "checkcondition" : "checkCondition",
                    "checkconstraint" : "checkConstraint",
                    "childoccurences" : "childOccurences",
                    "citableclassificationsystem" : "citableClassificationSystem",
                    "cloudcoverpercentage" : "cloudCoverPercentage",
                    "codedefinition" : "codeDefinition",
                    "codeexplanation" : "codeExplanation",
                    "codesetname" : "codesetName",
                    "codeseturl" : "codesetURL",
                    "collapsedelimiters" : "collapseDelimiters",
                    "communicationtype" : "communicationType",
                    "compressiongenerationquality" : "compressionGenerationQuality",
                    "compressionmethod" : "compressionMethod",
                    "conferencedate" : "conferenceDate",
                    "conferencelocation" : "conferenceLocation",
                    "conferencename" : "conferenceName",
                    "conferenceproceedings" : "conferenceProceedings",
                    "constraintdescription" : "constraintDescription",
                    "constraintname" : "constraintName",
                    "constanttosi" : "constantToSI",
                    "controlpoint" : "controlPoint",
                    "cornerpoint" : "cornerPoint",
                    "customunit" : "customUnit",
                    "dataformat" : "dataFormat",
                    "datasetgpolygon" : "datasetGPolygon",
                    "datasetgpolygonoutergring" : "datasetGPolygonOuterGRing",
                    "datasetgpolygonexclusiongring" : "datasetGPolygonExclusionGRing",
                    "datatable" : "dataTable",
                    "datatype" : "dataType",
                    "datetime" : "dateTime",
                    "datetimedomain" : "dateTimeDomain",
                    "datetimeprecision" : "dateTimePrecision",
                    "defaultvalue" : "defaultValue",
                    "definitionattributereference" : "definitionAttributeReference",
                    "denomflatratio" : "denomFlatRatio",
                    "depthsysdef" : "depthSysDef",
                    "depthdatumname" : "depthDatumName",
                    "depthdistanceunits" : "depthDistanceUnits",
                    "depthencodingmethod" : "depthEncodingMethod",
                    "depthresolution" : "depthResolution",
                    "descriptorvalue" : "descriptorValue",
                    "dictref" : "dictRef",
                    "diskusage" : "diskUsage",
                    "domainDescription" : "domainDescription",
                    "editedbook" : "editedBook",
                    "encodingmethod" : "encodingMethod",
                    "endcondition" : "endCondition",
                    "entitycodelist" : "entityCodeList",
                    "entitydescription" : "entityDescription",
                    "entityname" : "entityName",
                    "entityreference" : "entityReference",
                    "entitytype" : "entityType",
                    "enumerateddomain" : "enumeratedDomain",
                    "errorbasis" : "errorBasis",
                    "errorvalues" : "errorValues",
                    "externalcodeset" : "externalCodeSet",
                    "externallydefinedformat" : "externallyDefinedFormat",
                    "fielddelimiter" : "fieldDelimiter",
                    "fieldstartcolumn" : "fieldStartColumn",
                    "fieldwidth" : "fieldWidth",
                    "filmdistortioninformationavailability" : "filmDistortionInformationAvailability",
                    "foreignkey" : "foreignKey",
                    "formatname" : "formatName",
                    "formatstring" : "formatString",
                    "formatversion" : "formatVersion",
                    "fractiondigits" : "fractionDigits",
                    "fundername" : "funderName",
                    "funderidentifier" : "funderIdentifier",
                    "gettingstarted" : "gettingStarted",
                    "gring" : "gRing",
                    "gringpoint" : "gRingPoint",
                    "gringlatitude" : "gRingLatitude",
                    "gringlongitude" : "gRingLongitude",
                    "geogcoordsys" : "geogCoordSys",
                    "geometricobjectcount" : "geometricObjectCount",
                    "georeferenceinfo" : "georeferenceInfo",
                    "highwavelength" : "highWavelength",
                    "horizontalaccuracy" : "horizontalAccuracy",
                    "horizcoordsysdef" : "horizCoordSysDef",
                    "horizcoordsysname" : "horizCoordSysName",
                    "identifiername" : "identifierName",
                    "illuminationazimuthangle" : "illuminationAzimuthAngle",
                    "illuminationelevationangle" : "illuminationElevationAngle",
                    "imagingcondition" : "imagingCondition",
                    "imagequalitycode" : "imageQualityCode",
                    "imageorientationangle" : "imageOrientationAngle",
                    "intellectualrights" : "intellectualRights",
                    "imagedescription" : "imageDescription",
                    "isbn" : "ISBN",
                    "issn" : "ISSN",
                    "joincondition" : "joinCondition",
                    "keywordtype" : "keywordType",
                    "languagevalue" : "LanguageValue",
                    "languagecodestandard" : "LanguageCodeStandard",
                    "lensdistortioninformationavailability" : "lensDistortionInformationAvailability",
                    "licensename" : "licenseName",
                    "licenseurl" : "licenseURL",
                    "linenumber" : "lineNumber",
                    "literalcharacter" : "literalCharacter",
                    "literallayout" : "literalLayout",
                    "literaturecited" : "literatureCited",
                    "lowwavelength" : "lowWaveLength",
                    "machineprocessor" : "machineProcessor",
                    "maintenanceupdatefrequency" : "maintenanceUpdateFrequency",
                    "matrixtype" : "matrixType",
                    "maxexclusive" : "maxExclusive",
                    "maxinclusive" : "maxInclusive",
                    "maxlength" : "maxLength",
                    "maxrecordlength" : "maxRecordLength",
                    "maxvalues" : "maxValues",
                    "measurementscale" : "measurementScale",
                    "metadatalist" : "metadataList",
                    "methodstep" : "methodStep",
                    "minexclusive" : "minExclusive",
                    "mininclusive" : "minInclusive",
                    "minlength" : "minLength",
                    "minvalues" : "minValues",
                    "missingvaluecode" : "missingValueCode",
                    "moduledocs" : "moduleDocs",
                    "modulename" : "moduleName",
                    "moduledescription" : "moduleDescription",
                    "multiband" : "multiBand",
                    "multipliertosi" : "multiplierToSI",
                    "nonnumericdomain" : "nonNumericDomain",
                    "notnullconstraint" : "notNullConstraint",
                    "notplanned" : "notPlanned",
                    "numberofbands" : "numberOfBands",
                    "numbertype" : "numberType",
                    "numericdomain" : "numericDomain",
                    "numfooterlines" : "numFooterLines",
                    "numheaderlines" : "numHeaderLines",
                    "numberofrecords" : "numberOfRecords",
                    "numberofvolumes" : "numberOfVolumes",
                    "numphysicallinesperrecord" : "numPhysicalLinesPerRecord",
                    "objectname" : "objectName",
                    "oldvalue" : "oldValue",
                    "operatingsystem" : "operatingSystem",
                    "orderattributereference" : "orderAttributeReference",
                    "originalpublication" : "originalPublication",
                    "otherentity" : "otherEntity",
                    "othermaintenanceperiod" : "otherMaintenancePeriod",
                    "parameterdefinition" : "parameterDefinition",
                    "packageid" : "packageId",
                    "pagerange" : "pageRange",
                    "parentoccurences" : "parentOccurences",
                    "parentsi" : "parentSI",
                    "peakresponse" : "peakResponse",
                    "personalcommunication" : "personalCommunication",
                    "physicallinedelimiter" : "physicalLineDelimiter",
                    "pointinpixel" : "pointInPixel",
                    "preferredmembernode" : "preferredMemberNode",
                    "preprocessingtypecode" : "preProcessingTypeCode",
                    "primarykey" : "primaryKey",
                    "primemeridian" : "primeMeridian",
                    "proceduralstep" : "proceduralStep",
                    "programminglanguage" : "programmingLanguage",
                    "projcoordsys" : "projCoordSys",
                    "projectionlist" : "projectionList",
                    "propertyuri" : "propertyURI",
                    "pubdate" : "pubDate",
                    "pubplace" : "pubPlace",
                    "publicationplace" : "publicationPlace",
                    "quantitativeaccuracyreport" : "quantitativeAccuracyReport",
                    "quantitativeaccuracyvalue" : "quantitativeAccuracyValue",
                    "quantitativeaccuracymethod" : "quantitativeAccuracyMethod",
                    "quantitativeattributeaccuracyassessment" : "quantitativeAttributeAccuracyAssessment",
                    "querystatement" : "queryStatement",
                    "quotecharacter" : "quoteCharacter",
                    "radiometricdataavailability" : "radiometricDataAvailability",
                    "rasterorigin" : "rasterOrigin",
                    "recommendedunits" : "recommendedUnits",
                    "recommendedusage" : "recommendedUsage",
                    "referencedkey" : "referencedKey",
                    "referencetype" : "referenceType",
                    "relatedentry" : "relatedEntry",
                    "relationshiptype" : "relationshipType",
                    "reportnumber" : "reportNumber",
                    "reprintedition" : "reprintEdition",
                    "researchproject" : "researchProject",
                    "researchtopic" : "researchTopic",
                    "recorddelimiter" : "recordDelimiter",
                    "referencepublication" : "referencePublication",
                    "revieweditem" : "reviewedItem",
                    "rowcolumnorientation" : "rowColumnOrientation",
                    "runtimememoryusage" : "runtimeMemoryUsage",
                    "samplingdescription" : "samplingDescription",
                    "scalefactor" : "scaleFactor",
                    "sequenceidentifier" : "sequenceIdentifier",
                    "semiaxismajor" : "semiAxisMajor",
                    "shortname" : "shortName",
                    "simpledelimited" : "simpleDelimited",
                    "spatialraster" : "spatialRaster",
                    "spatialreference" : "spatialReference",
                    "spatialvector" : "spatialVector",
                    "standalone" : "standAlone",
                    "standardunit" : "standardUnit",
                    "startcondition" : "startCondition",
                    "studyareadescription" : "studyAreaDescription",
                    "storagetype" : "storageType",
                    "studyextent" : "studyExtent",
                    "studytype" : "studyType",
                    "textdelimited" : "textDelimited",
                    "textdomain" : "textDomain",
                    "textfixed" : "textFixed",
                    "textformat" : "textFormat",
                    "topologylevel" : "topologyLevel",
                    "tonegradation" : "toneGradation",
                    "totaldigits" : "totalDigits",
                    "totalfigures" : "totalFigures",
                    "totalpages" : "totalPages",
                    "totaltables" : "totalTables",
                    "triangulationindicator" : "triangulationIndicator",
                    "typesystem" : "typeSystem",
                    "uniquekey" : "uniqueKey",
                    "unittype" : "unitType",
                    "unitlist" : "unitList",
                    "usagecitation" : "usageCitation",
                    "valueuri" : "valueURI",
                    "valueattributereference" : "valueAttributeReference",
                    "verticalaccuracy" : "verticalAccuracy",
                    "vertcoordsys" : "vertCoordSys",
                    "virtualmachine" : "virtualMachine",
                    "wavelengthunits" : "waveLengthUnits",
                    "whitespace" : "whiteSpace",
                    "xintercept" : "xIntercept",
                    "xcoordinate" : "xCoordinate",
                    "xsi:schemalocation" : "xsi:schemaLocation",
                    "xslope" : "xSlope",
                    "ycoordinate" : "yCoordinate",
                    "yintercept" : "yIntercept",
                    "yslope" : "ySlope"
                  }
              );
            },

            /**
            * Fetch the EML from the MN object service
            * @param {object} [options] - A set of options for this fetch()
            * @property {boolean} [options.systemMetadataOnly=false] - If true, only the system metadata will be fetched.
            * If false, the system metadata AND EML document will be fetched.
            */
            fetch: function(options) {
              if( ! options ) var options = {};

              //Add the authorization header and other AJAX settings
               _.extend(options, MetacatUI.appUserModel.createAjaxSettings(), {dataType: "text"});

                // Merge the system metadata into the object first
                _.extend(options, {merge: true});
                this.fetchSystemMetadata(options);

                //If we are retrieving system metadata only, then exit now
                if(options.systemMetadataOnly)
                  return;

              //Call Backbone.Model.fetch to retrieve the info
                return Backbone.Model.prototype.fetch.call(this, options);

            },

            /*
             Deserialize an EML 2.1.1 XML document
            */
            parse: function(response) {
              // Save a reference to this model for use in setting the
              // parentModel inside anonymous functions
              var model = this;

              //If the response is XML
              if((typeof response == "string") && response.indexOf("<") == 0){
                //Look for a system metadata tag and call DataONEObject parse instead
                if(response.indexOf("systemMetadata>") > -1)
                  return DataONEObject.prototype.parse.call(this, response);

                response = this.cleanUpXML(response);
                    response = this.dereference(response);
                this.set("objectXML", response);
                var emlElement = $($.parseHTML(response)).filter("eml\\:eml");
              }

              var datasetEl;
              if(emlElement[0])
                datasetEl = $(emlElement[0]).find("dataset");

              if(!datasetEl || !datasetEl.length)
                return {};

              var emlParties = ["metadataprovider", "associatedparty", "creator", "contact", "publisher"],
                  emlDistribution = ["distribution"],
                  emlEntities = ["datatable", "otherentity", "spatialvector", "spatialraster", "storedprocedure", "view"],
                  emlText = ["abstract", "additionalinfo"],
                  emlMethods = ["methods"];

              var nodes = datasetEl.children(),
                  modelJSON = {};

              for(var i=0; i<nodes.length; i++){

                var thisNode = nodes[i];
                var convertedName = this.nodeNameMap()[thisNode.localName] || thisNode.localName;

                //EML Party modules are stored in EMLParty models
                if(_.contains(emlParties, thisNode.localName)){
                  if(thisNode.localName == "metadataprovider")
                    var attributeName = "metadataProvider";
                  else if(thisNode.localName == "associatedparty")
                    var attributeName = "associatedParty";
                  else
                    var attributeName = thisNode.localName;

                  if(typeof modelJSON[attributeName] == "undefined") modelJSON[attributeName] = [];

                  modelJSON[attributeName].push(new EMLParty({
                    objectDOM: thisNode,
                    parentModel: model,
                    type: attributeName
                  }));
                }
                //EML Distribution modules are stored in EMLDistribution models
                else if(_.contains(emlDistribution, thisNode.localName)){
                  if(typeof modelJSON[thisNode.localName] == "undefined") modelJSON[thisNode.localName] = [];

                  modelJSON[thisNode.localName].push(new EMLDistribution({
                    objectDOM: thisNode,
                    parentModel: model
                  }));
                }
                //The EML Project is stored in the EMLProject model
                else if(thisNode.localName == "project"){

                  modelJSON.project = new EMLProject({
                    objectDOM: thisNode,
                    parentModel: model
                   });

                }
                //EML Temporal, Taxonomic, and Geographic Coverage modules are stored in their own models
                else if(thisNode.localName == "coverage"){

                  var temporal = $(thisNode).children("temporalcoverage"),
                    geo      = $(thisNode).children("geographiccoverage"),
                    taxon    = $(thisNode).children("taxonomiccoverage");

                  if(temporal.length){
                    modelJSON.temporalCoverage = [];

                    _.each(temporal, function(t){
                      modelJSON.temporalCoverage.push(new EMLTemporalCoverage({
                        objectDOM: t,
                        parentModel: model
                          }));
                    });
                  }

                  if(geo.length){
                    modelJSON.geoCoverage = [];
                    _.each(geo, function(g){
                        modelJSON.geoCoverage.push(new EMLGeoCoverage({
                          objectDOM: g,
                          parentModel: model
                          }));
                    });

                  }

                  if(taxon.length){
                    modelJSON.taxonCoverage = [];
                    _.each(taxon, function(t){
                        modelJSON.taxonCoverage.push(new EMLTaxonCoverage({
                          objectDOM: t,
                          parentModel: model
                            }));
                    });

                  }

                }
                //Parse EMLText modules
                else if(_.contains(emlText, thisNode.localName)){
                  if(typeof modelJSON[convertedName] == "undefined") modelJSON[convertedName] = [];

                  modelJSON[convertedName].push(new EMLText({
                      objectDOM: thisNode,
                      parentModel: model
                    }));

                }
              else if(_.contains(emlMethods, thisNode.localName)) {
                if(typeof modelJSON[thisNode.localName] === "undefined") modelJSON[thisNode.localName] = [];

                modelJSON[thisNode.localName] =  new EMLMethods({
                  objectDOM: thisNode,
                  parentModel: model
                });

              }
              //Parse keywords
              else if(thisNode.localName == "keywordset"){
                //Start an array of keyword sets
                if(typeof modelJSON["keywordSets"] == "undefined") modelJSON["keywordSets"] = [];

                modelJSON["keywordSets"].push(new EMLKeywordSet({
                  objectDOM: thisNode,
                  parentModel: model
                }));
              }
              //Parse intellectual rights
              else if(thisNode.localName == "intellectualrights"){
                var value = "";

                if($(thisNode).children("para").length == 1)
                  value = $(thisNode).children("para").first().text().trim();
                else
                  $(thisNode).text().trim();

                //If the value is one of our pre-defined options, then add it to the model
                //if(_.contains(this.get("intellRightsOptions"), value))
                modelJSON["intellectualRights"] = value;

              }
              //Parse Entities
              else if(_.contains(emlEntities, thisNode.localName)){

                //Start an array of Entities
                if(typeof modelJSON["entities"] == "undefined")
                  modelJSON["entities"] = [];

                //Create the model
                var entityModel;
                if(thisNode.localName == "otherentity"){
                  entityModel = new EMLOtherEntity({
                      objectDOM: thisNode,
                      parentModel: model
                    }, {
                      parse: true
                    });
                } else if ( thisNode.localName == "datatable") {
                    entityModel = new EMLDataTable({
                        objectDOM: thisNode,
                        parentModel: model
                    }, {
                        parse: true
                    });
                }
                else {
                  entityModel = new EMLEntity({
                      objectDOM: thisNode,
                      parentModel: model,
                      entityType: "application/octet-stream",
                      type: thisNode.localName
                    }, {
                      parse: true
                    });
                }

                modelJSON["entities"].push(entityModel);
              }
              //Parse dataset-level annotations
              else if (thisNode.localName === "annotation") {
                if( !modelJSON["annotations"] ) {
                  modelJSON["annotations"] = new EMLAnnotations();
                }

                var annotationModel = new EMLAnnotation({
                  objectDOM: thisNode
                }, { parse: true });

                modelJSON["annotations"].add(annotationModel);
              }
              else{
                //Is this a multi-valued field in EML?
                if(Array.isArray(this.get(convertedName))){
                  //If we already have a value for this field, then add this value to the array
                  if(Array.isArray(modelJSON[convertedName]))
                    modelJSON[convertedName].push(this.toJson(thisNode));
                  //If it's the first value for this field, then create a new array
                  else
                    modelJSON[convertedName] = [this.toJson(thisNode)];
                }
                else
                  modelJSON[convertedName] = this.toJson(thisNode);
              }

            }

            return modelJSON;
          },

          /*
           * Retireves the model attributes and serializes into EML XML, to produce the new or modified EML document.
           * Returns the EML XML as a string.
           */
          serialize: function(){
            //Get the EML document
            var xmlString   = this.get("objectXML"),
                html        = $.parseHTML(xmlString),
                eml         = $(html).filter("eml\\:eml"),
                datasetNode = $(eml).find("dataset");

            //Update the packageId on the eml node with the EML id
            $(eml).attr("packageId", this.get("id"));

            // Set id attribute on dataset node if needed
            if (this.get("xmlID")) {
              $(datasetNode).attr("id", this.get("xmlID"));
            }

            // Set schema version
            $(eml).attr("xmlns:eml",
              MetacatUI.appModel.get("editorSerializationFormat") ||
              "https://eml.ecoinformatics.org/eml-2.2.0");

            // Set formatID
            this.set("formatId",
              MetacatUI.appModel.get("editorSerializationFormat") ||
              "https://eml.ecoinformatics.org/eml-2.2.0");

            // Ensure xsi:schemaLocation has a value for the current format
            eml = this.setSchemaLocation(eml);

            var nodeNameMap = this.nodeNameMap();

            //Serialize the basic text fields
            var basicText = ["alternateIdentifier", "title"];
            _.each(basicText, function(fieldName){
            var basicTextValues = this.get(fieldName);

            if(!Array.isArray(basicTextValues))
              basicTextValues = [basicTextValues];

            // Remove existing nodes
            datasetNode.children(fieldName.toLowerCase()).remove();

            // Create new nodes
            var nodes = _.map(basicTextValues, function(value) {

              if(value){

                var node = document.createElement(fieldName.toLowerCase());
                $(node).text(value);
                return node;

              }
              else{
                return "";
              }
            });

            var insertAfter = this.getEMLPosition(eml, fieldName.toLowerCase());

            if(insertAfter){
              insertAfter.after(nodes);
            }
            else{
              datasetNode.prepend(nodes);
            }
          }, this);

          // Serialize pubDate
          // This one is special because it has a default behavior, unlike
          // the others: When no pubDate is set, it should be set to
          // the current year
          var pubDate = this.get('pubDate');

          datasetNode.find('pubdate').remove();

          if (pubDate != null && pubDate.length > 0) {

            var pubDateEl = document.createElement('pubdate');

            $(pubDateEl).text(pubDate);

            this.getEMLPosition(eml, 'pubdate').after(pubDateEl);
          }

          // Serialize the parts of EML that are eml-text modules
          var textFields = ["abstract", "additionalInfo"];

          _.each(textFields, function(field){

            var fieldName = this.nodeNameMap()[field] || field;

            // Get the EMLText model
            var emlTextModels = Array.isArray(this.get(field)) ? this.get(field) : [this.get(field)];
            if( ! emlTextModels.length ) return;

            // Get the node from the EML doc
            var nodes = datasetNode.find(fieldName);

            // Update the DOMs for each model
            _.each(emlTextModels, function(thisTextModel, i){
              //Don't serialize falsey values
              if(!thisTextModel) return;

              var node;

              //Get the existing node or create a new one
              if(nodes.length < i+1){
                node = document.createElement(fieldName);
                this.getEMLPosition(eml, fieldName).after(node);

              }
              else {
                 node = nodes[i];
              }

              $(node).html( $(thisTextModel.updateDOM() ).html());

            }, this);

            // Remove the extra nodes
            this.removeExtraNodes(nodes, emlTextModels);

          }, this);

          //Create a <coverage> XML node if there isn't one
          if( datasetNode.children('coverage').length === 0 ) {
            var coverageNode = $(document.createElement('coverage')),
                coveragePosition = this.getEMLPosition(eml, 'coverage');

            if(coveragePosition)
              coveragePosition.after(coverageNode);
            else
              datasetNode.append(coverageNode);
          }
          else{
            var coverageNode = datasetNode.children("coverage").first();
          }

          //Serialize the geographic coverage
          if ( typeof this.get('geoCoverage') !== 'undefined' && this.get('geoCoverage').length > 0) {

            // Don't serialize if geoCoverage is invalid
            var validCoverages = _.filter(this.get('geoCoverage'), function(cov) {
              return cov.isValid();
            });

            //Get the existing geo coverage nodes from the EML
            var existingGeoCov = datasetNode.find("geographiccoverage");

            //Update the DOM of each model
            _.each(validCoverages, function(cov, position){

              //Update the existing node if it exists
              if(existingGeoCov.length-1 >= position){
                $(existingGeoCov[position]).replaceWith(cov.updateDOM());
              }
              //Or, append new nodes
              else{
                var insertAfter = existingGeoCov.length? datasetNode.find("geographiccoverage").last() : null;

                if(insertAfter)
                  insertAfter.after(cov.updateDOM());
                else
                  coverageNode.append(cov.updateDOM());
              }
            }, this);

            //Remove existing taxon coverage nodes that don't have an accompanying model
            this.removeExtraNodes(datasetNode.find("geographiccoverage"), validCoverages);
          }
          else{
            //If there are no geographic coverages, remove the nodes
            coverageNode.children("geographiccoverage").remove();
          }

          //Serialize the taxonomic coverage
          if ( typeof this.get('taxonCoverage') !== 'undefined' && this.get('taxonCoverage').length > 0) {

            // Group the taxonomic coverage models into empty and non-empty
            var sortedTaxonModels = _.groupBy(this.get('taxonCoverage'), function(t) {
              if( _.flatten(t.get('taxonomicClassification')).length > 0 ){
                return "notEmpty";
              }
              else{
                return "empty";
              }
            });

            //Get the existing taxon coverage nodes from the EML
            var existingTaxonCov = coverageNode.children("taxonomiccoverage");

            //Iterate over each taxon coverage and update it's DOM
            if(sortedTaxonModels["notEmpty"] && sortedTaxonModels["notEmpty"].length > 0) {

              //Update the DOM of each model
              _.each(sortedTaxonModels["notEmpty"], function(taxonCoverage, position){

                //Update the existing taxonCoverage node if it exists
                if(existingTaxonCov.length-1 >= position){
                  $(existingTaxonCov[position]).replaceWith(taxonCoverage.updateDOM());
                }
                //Or, append new nodes
                else{
                  coverageNode.append(taxonCoverage.updateDOM());
                }
              });

              //Remove existing taxon coverage nodes that don't have an accompanying model
              this.removeExtraNodes(existingTaxonCov, this.get("taxonCoverage"));

            }
            //If all the taxon coverages are empty, remove the parent taxonomicCoverage node
            else if( !sortedTaxonModels["notEmpty"] || sortedTaxonModels["notEmpty"].length == 0 ){
              existingTaxonCov.remove();
            }

          }

          //Serialize the temporal coverage
          var existingTemporalCoverages = datasetNode.find("temporalcoverage");

          //Update the DOM of each model
          _.each(this.get("temporalCoverage"), function(temporalCoverage, position){

            //Update the existing temporalCoverage node if it exists
            if(existingTemporalCoverages.length-1 >= position){
              $(existingTemporalCoverages[position]).replaceWith(temporalCoverage.updateDOM());
            }
            //Or, append new nodes
            else{
              coverageNode.append(temporalCoverage.updateDOM());
            }
          });

          //Remove existing taxon coverage nodes that don't have an accompanying model
          this.removeExtraNodes(existingTemporalCoverages, this.get("temporalCoverage"));

          //Remove the temporal coverage if it is empty
          if( !coverageNode.children("temporalcoverage").children().length ){
            coverageNode.children("temporalcoverage").remove();
          }

          //Remove the <coverage> node if it's empty
          if(coverageNode.children().length == 0){
            coverageNode.remove();
          }

          // Dataset-level annotations
          datasetNode.children("annotation").remove();

          if( this.get("annotations") ){
            this.get("annotations").each(function(annotation) {
              if (annotation.isEmpty()) {
                return;
              }

              var after = this.getEMLPosition(eml, "annotation");

              $(after).after(annotation.updateDOM());
            }, this);

            //Since there is at least one annotation, the dataset node needs to have an id attribute.
            datasetNode.attr("id", this.getUniqueEntityId(this));
          }

          //If there is no creator, create one from the user
          if(!this.get("creator").length){
           var party = new EMLParty({ parentModel: this, type: "creator" });

           party.createFromUser();

           this.set("creator", [party]);
          }

          //Serialize the creators
          this.serializeParties(eml, "creator");

          //Serialize the metadata providers
          this.serializeParties(eml, "metadataProvider");

          //Serialize the associated parties
          this.serializeParties(eml, "associatedParty");

          //Serialize the contacts
          this.serializeParties(eml, "contact");

          //Serialize the publishers
          this.serializeParties(eml, "publisher");

          // Serialize methods
          if(this.get('methods')) {

            //If the methods model is empty, remove it from the EML
            if( this.get("methods").isEmpty() )
              datasetNode.find("methods").remove();
            else{

              //Serialize the methods model
              var methodsEl = this.get('methods').updateDOM();

              //If the methodsEl is an empty string or other falsey value, then remove the methods node
              if( !methodsEl || !$(methodsEl).children().length ){
                datasetNode.find("methods").remove();
              }
              else{

                //Add the <methods> node to the EML
                datasetNode.find("methods").detach();

                var insertAfter = this.getEMLPosition(eml, "methods");

                if(insertAfter)
                  insertAfter.after(methodsEl);
                else
                  datasetNode.append(methodsEl);
              }
            }
          }
          //If there are no methods, then remove the methods nodes
          else{

            if( datasetNode.find("methods").length > 0 ){
              datasetNode.find("methods").remove();
            }

          }

          //Serialize the keywords
          this.serializeKeywords(eml, "keywordSets");

          //Serialize the intellectual rights
          if(this.get("intellectualRights")){
            if(datasetNode.find("intellectualRights").length)
              datasetNode.find("intellectualRights").html("<para>" + this.get("intellectualRights") + "</para>")
            else{

              this.getEMLPosition(eml, "intellectualrights").after(
                  $(document.createElement("intellectualRights"))
                    .html("<para>" + this.get("intellectualRights") + "</para>"));
            }
          }

          //Detach the project elements from the DOM
          if(datasetNode.find("project").length){

            datasetNode.find("project").detach();

          }

          //If there is an EMLProject, update its DOM
          if(this.get("project")){

            this.getEMLPosition(eml, "project").after(this.get("project").updateDOM());

          }

          //Get the existing taxon coverage nodes from the EML
          var existingEntities = datasetNode.find("otherEntity, dataTable, spatialRaster, spatialVector, storedProcedure, view");

          //Serialize the entities
          _.each(this.get("entities"), function(entity, position) {

            //Update the existing node if it exists
            if(existingEntities.length - 1 >= position) {
              //Remove the entity from the EML
              $(existingEntities[position]).detach();
              //Insert it into the correct position
              this.getEMLPosition(eml, entity.get("type").toLowerCase()).after(entity.updateDOM());        }
            //Or, append new nodes
            else {
              //Inser the entity into the correct position
              this.getEMLPosition(eml, entity.get("type").toLowerCase()).after(entity.updateDOM());
            }

          }, this);

          //Remove extra entities that have been removed
          var numExtraEntities = existingEntities.length - this.get("entities").length;
          for( var i = (existingEntities.length - numExtraEntities); i<existingEntities.length; i++){
            $(existingEntities)[i].remove();
          }

          //Do a final check to make sure there are no duplicate ids in the EML
          var elementsWithIDs = $(eml).find("[id]"),
          //Get an array of all the ids in this EML doc
              allIDs = _.map(elementsWithIDs, function(el){ return $(el).attr("id") });

          //If there is at least one id in the EML...
          if(allIDs && allIDs.length){
            //Boil the array down to just the unique values
            var uniqueIDs = _.uniq(allIDs);

            //If the unique array is shorter than the array of all ids,
            // then there is a duplicate somewhere
            if(uniqueIDs.length < allIDs.length){

              //For each element in the EML that has an id,
              _.each(elementsWithIDs, function(el){

                //Get the id for this element
                var id = $(el).attr("id");

                //If there is more than one element in the EML with this id,
                if( $(eml).find("[id='" + id + "']").length > 1 ){
                  //And if it is not a unit node, which we don't want to change,
                  if( !$(el).is("unit") )
                    //Then change the id attribute to a random uuid
                    $(el).attr("id", "urn-uuid-" + uuid.v4());
                }

              });

            }
          }

          //Camel-case the XML
          var emlString = "";
          _.each(html, function(rootEMLNode){ emlString += this.formatXML(rootEMLNode); }, this);

          return emlString;
        },

        /*
         * Given an EML DOM and party type, this function updated and/or adds the EMLParties to the EML
         */
        serializeParties: function(eml, type){

          //Remove the nodes from the EML for this party type
          $(eml).children("dataset").children(type.toLowerCase()).remove();

          //Serialize each party of this type
           _.each(this.get(type), function(party, i){

             //Get the last node of this type to insert after
             var insertAfter = $(eml).children("dataset").children(type.toLowerCase()).last();

             //If there isn't a node found, find the EML position to insert after
             if( !insertAfter.length ) {
               insertAfter = this.getEMLPosition(eml, type);
             }

             //Update the DOM of the EMLParty
             var emlPartyDOM = party.updateDOM();

             //Make sure we don't insert empty EMLParty nodes into the EML
             if( $(emlPartyDOM).children().length ){
               //Insert the party DOM at the insert position
                    if ( insertAfter && insertAfter.length )
                      insertAfter.after(emlPartyDOM);
                    //If an insert position still hasn't been found, then just append to the dataset node
                    else
                      $(eml).find("dataset").append(emlPartyDOM);
                }

           }, this);

          //Create a certain parties from the current app user if none is given
          if(type == "contact" && !this.get("contact").length){
            //Get the creators
            var creators = this.get("creator"),
              contacts = [];

            _.each(creators, function(creator){
              //Clone the creator model and add it to the contacts array
              var newModel = new EMLParty({ parentModel: this });
              newModel.set(creator.toJSON());
              newModel.set("type", type);

              contacts.push(newModel);
            }, this);

             this.set(type, contacts);

             //Call this function again to serialize the new models
             this.serializeParties(eml, type);
           }
         },

          serializeKeywords: function(eml) {
            // Remove all existing keywordSets before appending
            $(eml).find('dataset').find('keywordset').remove();

            if (this.get('keywordSets').length == 0) return;

            // Create the new keywordSets nodes
            var nodes = _.map(this.get('keywordSets'), function(kwd) {
              return kwd.updateDOM();
            });

                this.getEMLPosition(eml, "keywordset").after(nodes);
          },

          /*
           * Remoes nodes from the EML that do not have an accompanying model
           * (Were probably removed from the EML by the user during editing)
           */
          removeExtraNodes: function(nodes, models){
            // Remove the extra nodes
             var extraNodes =  nodes.length - models.length;
             if(extraNodes > 0){
               for(var i = models.length; i < nodes.length; i++){
                 $(nodes[i]).remove();
               }
             }
          },

          /*
           * Saves the EML document to the server using the DataONE API
           */
          save: function(attributes, options){

            //Validate before we try anything else
            if(!this.isValid()){
              this.trigger("invalid");
              this.trigger("cancelSave");
              return false;
            }
            else{
              this.trigger("valid");
            }

            this.setFileName();

            //Set the upload transfer as in progress
            this.set("uploadStatus", "p");

            //Reset the draftSaved attribute
            this.set("draftSaved", false);

            //Create the creator from the current user if none is provided
            if(!this.get("creator").length){
             var party = new EMLParty({ parentModel: this, type: "creator" });

             party.createFromUser();

             this.set("creator", [party]);
            }

            //Create the contact from the current user if none is provided
            if(!this.get("contact").length){
             var party = new EMLParty({ parentModel: this, type: "contact" });

             party.createFromUser();

             this.set("contact", [party]);
            }

            //If this is an existing object and there is no system metadata, retrieve it
            if(!this.isNew() && !this.get("sysMetaXML")){
              var model = this;

              //When the system metadata is fetched, try saving again
              var fetchOptions = {
                 success: function(response){
                   model.set(DataONEObject.prototype.parse.call(model, response));
                   model.save(attributes, options);
                 }
              }

              //Fetch the system metadata now
              this.fetchSystemMetadata(fetchOptions);

              return;
            }

           //Create a FormData object to send data with our XHR
           var formData = new FormData();

           try{

             //Add the identifier to the XHR data
            if(this.isNew()){
              formData.append("pid", this.get("id"));
            }
            else{
              //Create a new ID
              this.updateID();

              //Add the ids to the form data
              formData.append("newPid", this.get("id"));
              formData.append("pid", this.get("oldPid"));
            }

            //Serialize the EML XML
            var xml = this.serialize();
            var xmlBlob = new Blob([xml], {type : 'application/xml'});

            //Get the size of the new EML XML
            this.set("size", xmlBlob.size);

            //Get the new checksum of the EML XML
            var checksum = md5(xml);
            this.set("checksum", checksum);
            this.set("checksumAlgorithm", "MD5");

            //Create the system metadata XML
            var sysMetaXML = this.serializeSysMeta();

            //Send the system metadata as a Blob
            var sysMetaXMLBlob = new Blob([sysMetaXML], {type : 'application/xml'});

            //Add the object XML and System Metadata XML to the form data
            //Append the system metadata first, so we can take advantage of Metacat's streaming multipart handler
            formData.append("sysmeta", sysMetaXMLBlob, "sysmeta");
            formData.append("object", xmlBlob);
          }
          catch(error){
             //Reset the identifier since we didn't actually update the object
             this.resetID();

             this.set("uploadStatus", "e");
             this.trigger("error");
             this.trigger("cancelSave");
             return false;
           }

           var model = this;
           var saveOptions = options || {};
           _.extend(saveOptions, {
             data : formData,
             cache: false,
             contentType: false,
             dataType: "text",
             processData: false,
             parse: false,
             //Use the URL function to determine the URL
             url: this.isNew() ? this.url() : this.url({update: true}),
             xhr: function(){
                var xhr = new window.XMLHttpRequest();

                //Upload progress
                xhr.upload.addEventListener("progress", function(evt){
                  if (evt.lengthComputable) {
                    var percentComplete = evt.loaded / evt.total * 100;

                    model.set("uploadProgress", percentComplete);
                  }
                }, false);

                return xhr;
            },
            success: function(model, response, xhr){

              model.set("numSaveAttempts", 0);
              model.set("uploadStatus", "c");
              model.set("sysMetaXML", model.serializeSysMeta());
              model.set("oldPid", null);
              model.fetch({merge: true, systemMetadataOnly: true});
              model.trigger("successSaving", model);

            },
            error: function(model, response, xhr){

              model.set("numSaveAttempts", model.get("numSaveAttempts") + 1);
              var numSaveAttempts = model.get("numSaveAttempts");

              //Reset the identifier changes
              model.resetID();

              if( numSaveAttempts < 3 && (response.status == 408 || response.status == 0) ){

                //Try saving again in 10, 40, and 90 seconds
                setTimeout(function(){
                            model.save.call(model);
                           },
                           (numSaveAttempts * numSaveAttempts) * 10000);
              }
              else{
                model.set("numSaveAttempts", 0);

                //Get the error error information
                var errorDOM       = $($.parseHTML(response.responseText)),
                    errorContainer = errorDOM.filter("error"),
                    msgContainer   = errorContainer.length? errorContainer.find("description") : errorDOM.not("style, title"),
                    errorMsg       = msgContainer.length? msgContainer.text() : errorDOM;

                //When there is no network connection (status == 0), there will be no response text
                if(!errorMsg || (response.status == 408 || response.status == 0))
                  errorMsg = "There was a network issue that prevented your metadata from uploading. " +
                         "Make sure you are connected to a reliable internet connection.";

                //Save the error message in the model
                model.set("errorMessage", errorMsg);

                //Set the model status as e for error
                model.set("uploadStatus", "e");

                //Save the EML as a plain text file, until drafts are a supported feature
                var copy = model.createTextCopy();

                //If the EML copy successfully saved, let the user know that there is a copy saved behind the scenes
                model.listenToOnce(copy, "successSaving", function(){

                  model.set("draftSaved", true);

                  //Trigger the errorSaving event so other parts of the app know that the model failed to save
                  //And send the error message with it
                  model.trigger("errorSaving", errorMsg);

                });

                //If the EML copy fails to save too, then just display the usual error message
                model.listenToOnce(copy, "errorSaving", function(){

                  //Trigger the errorSaving event so other parts of the app know that the model failed to save
                  //And send the error message with it
                  model.trigger("errorSaving", errorMsg);

                });

                //Save the EML plain text copy
                copy.save();

                //Send this exception to Google Analytics
                if(MetacatUI.appModel.get("googleAnalyticsKey") && (typeof ga !== "undefined")){
                  ga('send', 'exception', {
                    'exDescription': "EML save error: " + errorMsg + " | Id: " + model.get("id") +
                      " | v. " + MetacatUI.metacatUIVersion + " | EML draft: " + copy.get("id"),
                    'exFatal': true
                  });
                }
              }
            }
         }, MetacatUI.appUserModel.createAjaxSettings());

          return Backbone.Model.prototype.save.call(this, attributes, saveOptions);
        },

          /*
           * Checks if this EML model has all the required values necessary to save to the server
           */
          validate: function() {
            let errors = {};

            //A title is always required by EML
            if( !this.get("title").length || !this.get("title")[0] ){
              errors.title = "A title is required";
            }

            // Validate the publication date
            if (this.get("pubDate") != null) {
              if (!this.isValidYearDate(this.get("pubDate"))) {
                errors["pubDate"] = ["The value entered for publication date, '"
                  + this.get("pubDate") +
                  "' is not a valid value for this field. Enter with a year (e.g. 2017) or a date in the format YYYY-MM-DD."]
              }
            }

            // Validate the temporal coverage
            errors.temporalCoverage = [];

            //If temporal coverage is required and there aren't any, return an error
            if( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage &&
                 !this.get("temporalCoverage").length ){
              errors.temporalCoverage = [{ beginDate:  "Provide a begin date." }];
            }
            //If temporal coverage is required and they are all empty, return an error
            else if( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage &&
                     _.every(this.get("temporalCoverage"), function(tc){
                       return tc.isEmpty();
                     }) ){
              errors.temporalCoverage = [{ beginDate:  "Provide a begin date." }];
            }
            //If temporal coverage is not required, validate each one
            else if( this.get("temporalCoverage").length ||
                      ( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage &&
                               _.every(this.get("temporalCoverage"), function(tc){
                                 return tc.isEmpty();
                               }) )) {
              //Iterate over each temporal coverage and add it's validation errors
              _.each(this.get("temporalCoverage"), function(temporalCoverage){
                if( !temporalCoverage.isValid() && !temporalCoverage.isEmpty() ){
                  errors.temporalCoverage.push(temporalCoverage.validationError);
                }
              });
            }

            //Remove the temporalCoverage attribute if no errors were found
            if( errors.temporalCoverage.length == 0 ){
              delete errors.temporalCoverage;
            }

            //Validate the EMLParty models
            var partyTypes = ["associatedParty", "contact", "creator", "metadataProvider", "publisher"];
            _.each(partyTypes, function(type){

              var people = this.get(type);
              _.each(people, function(person, i){

                if( !person.isValid() ){
                  if( !errors[type] )
                    errors[type] = [person.validationError];
                  else
                    errors[type].push(person.validationError);
                }

              }, this);

            }, this);

            //Validate the EMLGeoCoverage models
            _.each(this.get("geoCoverage"), function(geoCoverageModel, i){

              if( !geoCoverageModel.isValid() ){
                if( !errors.geoCoverage )
                  errors.geoCoverage = [geoCoverageModel.validationError];
                else
                  errors.geoCoverage.push(geoCoverageModel.validationError);
              }

            }, this);

            //Validate the EMLTaxonCoverage model
            var taxonModel = this.get("taxonCoverage")[0];

            if( !taxonModel.isEmpty() && !taxonModel.isValid() ){
              errors = _.extend(errors, taxonModel.validationError);
            }
            else if( taxonModel.isEmpty() &&
              this.get("taxonCoverage").length == 1 &&
              MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage ){

              taxonModel.isValid();
              errors = _.extend(errors, taxonModel.validationError);

            }

            //Validate each EMLEntity model
            _.each( this.get("entities"), function(entityModel){

              if( !entityModel.isValid() ){
                if( !errors.entities )
                  errors.entities = [entityModel.validationError];
                else
                  errors.entities.push(entityModel.validationError);
              }

            });

            //Validate the EML Methods
            let emlMethods = this.get("methods");
            if( emlMethods ){
              if( !emlMethods.isValid() ){
                errors.methods = emlMethods.validationError;
              }
            }

            // Validate each EMLAnnotation model
            if( this.get("annotations") ){
              this.get("annotations").each(function (model) {
                if (model.isValid()) {
                  return;
                }

                if (!errors.annotations) {
                  errors.annotations = [];
                }

                errors.annotations.push(model.validationError);
              }, this);
            }

            //Check the required fields for this MetacatUI configuration
            for([field, isRequired] of Object.entries(MetacatUI.appModel.get("emlEditorRequiredFields"))){

              //If it's not required, then go to the next field
              if(!isRequired) continue;

              if(field == "alternateIdentifier"){
                    if( !this.get("alternateIdentifier").length || _.every(this.get("alternateIdentifier"), function(altId){ return altId.trim() == "" }) )
                      errors.alternateIdentifier = "At least one alternate identifier is required."
                  }
              else if(field == "generalTaxonomicCoverage"){
                    if( !this.get("taxonCoverage").length || !this.get("taxonCoverage")[0].get("generalTaxonomicCoverage") )
                      errors.generalTaxonomicCoverage = "Provide a description of the general taxonomic coverage of this data set.";
                  }
              else if(field == "geoCoverage"){
                    if(!this.get("geoCoverage").length)
                      errors.geoCoverage = "At least one location is required.";
                  }
              else if(field == "intellectualRights"){
                    if( !this.get("intellectualRights") )
                      errors.intellectualRights = "Select usage rights for this data set.";
                  }
              else if(field == "studyExtentDescription"){
                    if( !this.get("methods") || !this.get("methods").get("studyExtentDescription") )
                      errors.studyExtentDescription = "Provide a study extent description.";
                  }
              else if(field == "samplingDescription"){
                    if( !this.get("methods") || !this.get("methods").get("samplingDescription") )
                      errors.samplingDescription = "Provide a sampling description.";
                  }
              else if(field == "temporalCoverage"){
                    if(!this.get("temporalCoverage").length)
                      errors.temporalCoverage = "Provide the date(s) for this data set.";
                  }
              else if(field == "taxonCoverage"){
                    if(!this.get("taxonCoverage").length)
                      errors.taxonCoverage = "At least one taxa rank and value is required.";
                  }
              else if(field == "keywordSets"){
                    if( !this.get("keywordSets").length )
                      errors.keywordSets = "Provide at least one keyword.";
                  }
                  //The EMLMethods model will validate itself for required fields, but
                  // this is a rudimentary check to make sure the EMLMethods model was created
                  // in the first place
              else if(field == "methods"){
                    if(!this.get("methods"))
                      errors.methods = "At least one method step is required.";
                  }
              else if(field == "funding"){
                    // Note: Checks for either the funding or award element. award
                    // element is checked by the project's objectDOM for now until
                    // EMLProject fully supports the award element
                    if(!this.get("project") ||
                       !(this.get("project").get("funding").length ||
                         (this.get("project").get("objectDOM") &&
                          this.get("project").get("objectDOM").querySelectorAll &&
                          this.get("project").get("objectDOM").querySelectorAll("award").length > 0)))
                      errors.funding = "Provide at least one project funding number or name.";
                  }
              else if(field == "abstract"){
                    if(!this.get("abstract").length)
                      errors["abstract"] = "Provide an abstract.";
                  }
              else if(field == "dataSensitivity"){
                    if( !this.getDataSensitivity() ){
                      errors["dataSensitivity"] = "Pick the category that best describes the level of sensitivity or restriction of the data.";
                    }
                  }
              //If this is an EMLParty type, check that there is a party of this type in the model
              else if( EMLParty.prototype.partyTypes.map(t=>t.dataCategory).includes(field) ){
                //If this is an associatedParty role
                if( EMLParty.prototype.defaults().roleOptions?.includes(field) ){
                  if(!this.get("associatedParty")?.map(p=>p.get("roles")).flat().includes(field)){
                    errors[field] = "Provide information about the people or organization(s) in the role: " +
                      EMLParty.prototype.partyTypes.find(t=>t.dataCategory==field)?.label;
                  }
                }
                else if( !this.get(field)?.length ){
                  errors[field] = "Provide information about the people or organization(s) in the role: " +
                      EMLParty.prototype.partyTypes.find(t=>t.dataCategory==field)?.label;
                }
              }
              else if( !this.get(field) || !this.get(field)?.length ){
                errors[field] = "Provide a " + field + ".";
              }
            }

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

          /* Returns a boolean for whether the argument 'value' is a valid
          value for EML's yearDate type which is used in a few places.

          Note that this method considers a zero-length String to be valid
          because the EML211.serialize() method will properly handle a null
          or zero-length String by serializing out the current year. */
          isValidYearDate: function(value) {
            return (value === "" || /^\d{4}$/.test(value) || /^\d{4}-\d{2}-\d{2}$/.test(value));
          },

          /*
           * Sends an AJAX request to fetch the system metadata for this EML object.
           * Will not trigger a sync event since it does not use Backbone.Model.fetch
           */
          fetchSystemMetadata: function(options){

            if(!options) var options = {};
            else options = _.clone(options);

            var model = this,
              fetchOptions = _.extend({
                url: MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(this.get("id")),
                dataType: "text",
                success: function(response){
                  model.set(DataONEObject.prototype.parse.call(model, response));

                  //Trigger a custom event that the sys meta was updated
                  model.trigger("sysMetaUpdated");
                },
                error: function(){
                  model.trigger('error');
                }
              }, options);

              //Add the authorization header and other AJAX settings
              _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings());

              $.ajax(fetchOptions);
          },
          /*
           * Returns the nofde in the given EML document that the given node type
           * should be inserted after
           *
           * Returns false if either the node is not found in the and this should
           * be handled by the caller.
           */
          getEMLPosition: function(eml, nodeName) {
            var nodeOrder = this.get("nodeOrder");
            var position = _.indexOf(nodeOrder, nodeName.toLowerCase());

            if (position == -1) {
              return false;
            }

            // 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 ($(eml).find("dataset").children(nodeOrder[i]).length) {
                return $(eml).find("dataset").children(nodeOrder[i]).last();
              }
            }

            return false;
          },

          /*
           * Checks if this model has updates that need to be synced with the server.
           */
          hasUpdates: function(){
            if(this.constructor.__super__.hasUpdates.call(this)) return true;

            //If nothing else has been changed, then this object hasn't had any updates
            return false;
          },

          /*
           Add an entity into the EML 2.1.1 object
          */
          addEntity: function(emlEntity, position) {
            //Get the current list of entities
            var currentEntities = this.get("entities");

            if( typeof position == "undefined" || position == -1)
              currentEntities.push(emlEntity);
            else
              //Add the entity model to the entity array
              currentEntities.splice(position, 0, emlEntity);

            this.trigger("change:entities");

            this.trickleUpChange();

            return this;
          },

          /*
           Remove an entity from the EML 2.1.1 object
          */
          removeEntity: function(emlEntity) {
              if(!emlEntity || typeof emlEntity != "object")
                return;

            //Get the current list of entities
            var entities = this.get("entities");

            entities = _.without(entities, emlEntity);

            this.set("entities", entities);
          },

          /*
           * Find the entity model for a given DataONEObject
           */
          getEntity: function(dataONEObj){

            //If an EMLEntity model has been found for this object before, then return it
            if( dataONEObj.get("metadataEntity") ){
              dataONEObj.get("metadataEntity").set("dataONEObject", dataONEObj);
              return dataONEObj.get("metadataEntity");
            }

            var entity = _.find(this.get("entities"), function(e){

              //Matches of the checksum or identifier are definite matches
              if( e.get("xmlID") == dataONEObj.getXMLSafeID() )
                return true;
              else if( e.get("physicalMD5Checksum") && (e.get("physicalMD5Checksum") == dataONEObj.get("checksum") && dataONEObj.get("checksumAlgorithm").toUpperCase() == "MD5"))
                return true;
              else if(e.get("downloadID") && e.get("downloadID") == dataONEObj.get("id"))
                return true;

              // Get the file name from the EML for this entity
              var fileNameFromEML = e.get("physicalObjectName") || e.get("entityName");

              // If the EML file name matches the DataONEObject file name
              if (fileNameFromEML &&
                  dataONEObj.get("fileName") &&
                  ((fileNameFromEML.toLowerCase() == dataONEObj.get("fileName").toLowerCase()) ||
                    (fileNameFromEML.replace(/ /g, "_").toLowerCase() == dataONEObj.get("fileName").toLowerCase()))) {

                //Get an array of all the other entities in this EML
                var otherEntities = _.without(this.get("entities"), e);

                  // If this entity name matches the dataone object file name, AND no other dataone object file name
                  // matches, then we can assume this is the entity element for this file.
                var otherMatchingEntity = _.find(otherEntities, function(otherE){

                  // Get the file name from the EML for the other entities
                  var otherFileNameFromEML = otherE.get("physicalObjectName") || otherE.get("entityName");

                  // If the file names match, return true
                  if( (otherFileNameFromEML == dataONEObj.get("fileName")) || (otherFileNameFromEML.replace(/ /g, "_") == dataONEObj.get("fileName")) )
                    return true;
                });

                // If this entity's file name didn't match any other file names in the EML,
                // then this entity is a match for the given dataONEObject
                if( !otherMatchingEntity )
                  return true;
              }

            }, this);

            //If we found an entity, give it an ID and return it
            if(entity){

              //If this entity has been matched to another DataONEObject already, then don't match it again
              if( entity.get("dataONEObject") == dataONEObj ){
                return entity;
              }
              //If this entity has been matched to a different DataONEObject already, then don't match it again.
              //i.e. We will not override existing entity<->DataONEObject pairings
              else if( entity.get("dataONEObject") ){
                return;
              }
              else{
                entity.set("dataONEObject", dataONEObj);
              }

                //Create an XML-safe ID and set it on the Entity model
                var entityID = this.getUniqueEntityId(dataONEObj);
                entity.set("xmlID", entityID);

                //Save a reference to this entity so we don't have to refind it later
                dataONEObj.set("metadataEntity", entity);

              return entity;
            }

            //See if one data object is of this type in the package
            var matchingTypes = _.filter(this.get("entities"), function(e){
              return (e.get("formatName") == (dataONEObj.get("formatId") || dataONEObj.get("mediaType")));
            });

            if(matchingTypes.length == 1){
                //Create an XML-safe ID and set it on the Entity model
              matchingTypes[0].set("xmlID", dataONEObj.getXMLSafeID());

              return matchingTypes[0];
            }

            //If this EML is in a DataPackage with only one other DataONEObject,
            // and there is only one entity in the EML, then we can assume they are the same entity
            if( this.get("entities").length == 1 ){

              if( this.get("collections")[0] && this.get("collections")[0].type == "DataPackage" &&
                  this.get("collections")[0].length == 2 && _.contains(this.get("collections")[0].models, dataONEObj)){
                    return this.get("entities")[0];
              }

            }

            return false;

          },

          createEntity: function(dataONEObject){
            // Add or append an entity to the parent's entity list
              var entityModel = new EMLOtherEntity({
                  entityName : dataONEObject.get("fileName"),
                  entityType : dataONEObject.get("formatId") ||
                               dataONEObject.get("mediaType") ||
                               "application/octet-stream",
                  dataONEObject: dataONEObject,
                  parentModel: this,
                  xmlID: dataONEObject.getXMLSafeID()
              });

              this.addEntity(entityModel);

              //If this DataONEObject fails to upload, remove the EML entity
              this.listenTo(dataONEObject, "errorSaving", function(){
                this.removeEntity(dataONEObject.get("metadataEntity"));

                //Listen for a successful save so the entity can be added back
                this.listenToOnce(dataONEObject, "successSaving", function(){
                  this.addEntity(dataONEObject.get("metadataEntity"))
                });
              });

          },

          /*
          * Creates an XML-safe identifier that is unique to this EML document,
          * based on the given DataONEObject model. It is intended for EML entity nodes in particular.
          *
          * @param {DataONEObject} - a DataONEObject model that this EML documents
          * @return {string} - an identifier string unique to this EML document
          */
          getUniqueEntityId: function(dataONEObject){

            var uniqueId = "";

            uniqueId = dataONEObject.getXMLSafeID();

            //Get the EML string, if there is one, to check if this id already exists
            var emlString = this.get("objectXML");

            //If this id already exists in the EML...
            if(emlString && emlString.indexOf(' id="' + uniqueId + '"')){
              //Create a random uuid to use instead
              uniqueId = "urn-uuid-" + uuid.v4();
            }

            return uniqueId;

          },

          /*
           * removeParty - removes the given EMLParty model from this EML211 model's attributes
           */
          removeParty: function(partyModel){
            //The list of attributes this EMLParty might be stored in
            var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"];

            // Iterate over each possible attribute
            _.each(possibleAttr, function(attr){

              if( _.contains(this.get(attr), partyModel) ){
                this.set( attr, _.without(this.get(attr), partyModel) );
              }

            }, this);
          },

          /**
           * Attempt to move a party one index forward within its sibling models
           *
           * @param {EMLParty} partyModel: The EMLParty model we're moving
           */
          movePartyUp: function(partyModel) {
            var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"];

            // Iterate over each possible attribute
            _.each(possibleAttr, function(attr){
              if (!_.contains(this.get(attr), partyModel)) {
                return;
              }
              // Make a clone because we're going to use splice
              var models = _.clone(this.get(attr));

              // Find the index of the model we're moving
              var index = _.findIndex(models, function(m) {
                return m === partyModel;
              });

              if (index === 0) {
                // Already first
                return;
              }

              if (index === -1) {
                // Couldn't find the model
                return;
              }

              // Do the move using splice and update the model
              models.splice(index - 1, 0, models.splice(index, 1)[0])
              this.set(attr, models);
              this.trigger("change:" + attr);
            }, this);
          },

          /**
           * Attempt to move a party one index forward within its sibling models
           *
           * @param {EMLParty} partyModel: The EMLParty model we're moving
           */
          movePartyDown: function(partyModel) {
            var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"];

            // Iterate over each possible attribute
            _.each(possibleAttr, function(attr){
              if (!_.contains(this.get(attr), partyModel)) {
                return;
              }
              // Make a clone because we're going to use splice
              var models = _.clone(this.get(attr));

              // Find the index of the model we're moving
              var index = _.findIndex(models, function(m) {
                return m === partyModel;
              });

              if (index === -1) {
                // Couldn't find the model
                return;
              }

              // Figure out where to put the new model
              //   Leave it in the same place if the next index doesn't exist
              //   Move one forward if it does
              var newIndex = (models.length <= index + 1) ? index : index + 1;

              // Do the move using splice and update the model
              models.splice(newIndex, 0, models.splice(index, 1)[0])
              this.set(attr, models);
              this.trigger("change:" + attr);
            }, this);
          },

          /*
          * Adds the given EMLParty model to this EML211 model in the
          * appropriate role array in the given position
          *
          * @param {EMLParty} - The EMLParty model to add
          * @param {number} - The position in the role array in which to insert this EMLParty
          * @return {boolean} - Returns true if the EMLParty was successfully added, false if it was cancelled
          */
          addParty: function(partyModel, position){

            //If the EMLParty model is empty, don't add it to the EML211 model
            if(partyModel.isEmpty())
              return false;

            //Get the role of this EMLParty
            var role = partyModel.get("type") || "associatedParty";

            //If this model already contains this EMLParty, then exit
            if( _.contains(this.get(role), partyModel) )
              return false;

            if( typeof position == "undefined" ){
              this.get(role).push(partyModel);
            }
            else {
              this.get(role).splice(position, 0, partyModel);
            }

            this.trigger("change:" + role);

            return true;
          },

          /**
           * getPartiesByType - Gets an array of EMLParty members that have a particular party type or role.
           * @param {string} partyType - A string that represents either the role or the party type. For example, "contact", "creator", "principalInvestigator", etc.
           * @since 2.15.0
           */
          getPartiesByType: function(partyType){

            try {
              if(!partyType){
                return false
              }
              var associatedPartyTypes = new EMLParty().get("roleOptions"),
                  isAssociatedParty = associatedPartyTypes.includes(partyType),
                  parties = [];
              // For "contact", "creator", "metadataProvider", "publisher", each party type has it's own
              // array in the EML model
              if(!isAssociatedParty){
                parties = this.get(partyType);
              // For "custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator", etc.,
              // party members are listed in the EML model's associated parties array. Each associated party's
              // party type is indicated in the role attribute.
              } else {
                parties = _.filter(this.get("associatedParty"), function (associatedParty) {
                  return associatedParty.get("roles").includes(partyType) }
                );
              }

              return parties;

            } catch (error) {
              console.log("Error trying to find a list of party members in an EML model by type. Error details: " + error);
            }
          },

          createUnits: function(){
            this.units.fetch();
          },

          /* Initialize the object XML for brand spankin' new EML objects */
          createXML: function() {
              var xml = "<eml:eml xmlns:eml=\"https://eml.ecoinformatics.org/eml-2.2.0\"></eml:eml>",
                  eml = $($.parseHTML(xml));

                  // Set base attributes
                  eml.attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
                  eml.attr("xmlns:stmml", "http://www.xml-cml.org/schema/stmml-1.1");
                  eml.attr("xsi:schemaLocation", "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd");
                  eml.attr("packageId", this.get("id"));
                  eml.attr("system", "knb"); // We could make this configurable at some point

                  // Add the dataset
                  eml.append(document.createElement("dataset"));
                  eml.find("dataset").append(document.createElement("title"));

                  var emlString = $(document.createElement("div")).append(eml.clone()).html();

                  return emlString;
          },

          /*
              Replace elements named "source" with "sourced" due to limitations
              with using $.parseHTML() rather than $.parseXML()

              @param xmlString  The XML string to make the replacement in
          */
          cleanUpXML: function(xmlString){
            xmlString.replace("<source>", "<sourced>");
            xmlString.replace("</source>", "</sourced>");

            return xmlString;
          },

          createTextCopy: function(){
            var emlDraftText = "EML draft for " + this.get("id") + "(" + this.get("title") + ") by " +
                  MetacatUI.appUserModel.get("firstName") + " " + MetacatUI.appUserModel.get("lastName");

            if(this.get("uploadStatus") == "e" && this.get("errorMessage")){
              emlDraftText += ". This EML had the following save error: `" + this.get("errorMessage") + "`   ";
            }
            else {
              emlDraftText += ":   ";
            }

            emlDraftText += this.serialize();

            var plainTextEML = new DataONEObject({
                  formatId: "text/plain",
                  fileName: "eml_draft_" + (MetacatUI.appUserModel.get("lastName") || "") + ".txt",
                  uploadFile: new Blob([emlDraftText], {type : 'plain/text'}),
                  synced: true
                });

            return plainTextEML;
          },

          /*
          * Cleans up the given text so that it is XML-valid by escaping reserved characters, trimming white space, etc.
          *
          * @param {string} textString - The string to clean up
          * @return {string} - The cleaned up string
          */
          cleanXMLText: function(textString){

            if( typeof textString != "string" )
              return;

            textString = textString.trim();

            //Check for XML/HTML elements
            _.each(textString.match(/<\s*[^>]*>/g), function(xmlNode){

              //Encode <, >, and </ substrings
              var tagName = xmlNode.replace(/>/g, "&gt;");
              tagName = tagName.replace(/</g, "&lt;");

              //Replace the xmlNode in the full text string
              textString = textString.replace(xmlNode, tagName);

            });

            //Remove Unicode characters that are not valid XML characters
            //Create a regular expression that matches any character that is not a valid XML character
            // (see https://www.w3.org/TR/xml/#charsets)
            var invalidCharsRegEx = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/g;
            textString = textString.replace(invalidCharsRegEx, "");

            return textString;

          },

          /*
              Dereference "reference" elements and replace them with a cloned copy
              of the referenced content

              @param xmlString  The XML string with reference elements to transform
          */
          dereference: function(xmlString) {
              var referencesList; // the array of references elements in the document
              var referencedID;  // The id of the referenced element
              var referencesParentEl;  // The parent of the given references element
              var referencedEl; // The referenced DOM to be copied

              var xmlDOM = $.parseXML(xmlString);
              referencesList = xmlDOM.getElementsByTagName("references");

              if (referencesList.length) {
                  // Process each references elements
                  _.each(referencesList, function(referencesEl, index, referencesList) {
                      // Can't rely on the passed referencesEl since the list length changes
                      // because of the remove() below. Reuse referencesList[0] for every item:
                      // referencedID = $(referencesEl).text(); // doesn't work
                      referencesEl = referencesList[0];
                      referencedID = $(referencesEl).text();
                      referencesParentEl = ($(referencesEl).parent())[0];
                      if (typeof referencedID !== "undefined" && referencedID != "") {
                          referencedEl = xmlDOM.getElementById(referencedID);
                          if (typeof referencedEl != "undefined") {
                              // Clone the referenced element and replace the references element
                              var referencedClone = ($(referencedEl).clone())[0];
                              $(referencesParentEl)
                                  .children(referencesEl.localName)
                                  .replaceWith($(referencedClone).children());
                              //$(referencesParentEl).append($(referencedClone).children());
                              $(referencesParentEl).attr("id", DataONEObject.generateId());
                          }
                      }
                  }, xmlDOM);
              }
              return (new XMLSerializer()).serializeToString(xmlDOM);
          },

          /*
          * Uses the EML `title` to set the `fileName` attribute on this model.
          */
          setFileName: function(){

            var title = "";

            // Get the title from the metadata
            if( Array.isArray(this.get("title")) ){
              title = this.get("title")[0];
            }
            else if( typeof this.get("title") == "string" ){
              title = this.get("title");
            }

            //Max title length
            var maxLength = 50;

            //trim the string to the maximum length
            var trimmedTitle = title.trim().substr(0, maxLength);

            //re-trim if we are in the middle of a word
            if( trimmedTitle.indexOf(" ") > -1 ){
              trimmedTitle = trimmedTitle.substr(0, Math.min(trimmedTitle.length, trimmedTitle.lastIndexOf(" ")));
            }

            //Replace all non alphanumeric characters with underscores
            // and make sure there isn't more than one underscore in a row
            trimmedTitle = trimmedTitle.replace(/[^a-zA-Z0-9]/g, "_").replace(/_{2,}/g, "_");

            //Set the fileName on the model
            this.set("fileName", trimmedTitle + ".xml");
          },

          trickleUpChange: function(){
            if( !MetacatUI.rootDataPackage || !MetacatUI.rootDataPackage.packageModel )
              return;

            //Mark the package as changed
            MetacatUI.rootDataPackage.packageModel.set("changed", true);
          },

          /**
           * Sets the xsi:schemaLocation attribute on the passed-in Element
           * depending on the application configuration.
           *
           * @param {Element} eml: The root eml:eml element to modify
           * @return {Element} The element, possibly modified
           */
          setSchemaLocation: function(eml) {
            if (!MetacatUI || !MetacatUI.appModel) {
              return eml;
            }

            var current = $(eml).attr("xsi:schemaLocation"),
                format = MetacatUI.appModel.get("editorSerializationFormat"),
                location = MetacatUI.appModel.get("editorSchemaLocation");

            // Return now if we can't do anything anyway
            if (!format || !location) {
              return eml;
            }

            // Simply add if the attribute isn't present to begin with
            if (!current || typeof current !== "string") {
              $(eml).attr("xsi:schemaLocation", format + " " + location);

              return eml;
            }

            // Don't append if it's already present
            if (current.indexOf(format) >= 0) {
              return eml;
            }

            $(eml).attr("xsi:schemaLocation", current + " " + location);

            return eml;
          },

          createID: function() {
            this.set("xmlID", uuid.v4());
          },

          /**
          * Creates and adds an {@link EMLAnnotation} to this EML211 model with the given annotation data in JSON form.
          * @param {object} annotationData The attribute data to set on the new {@link EMLAnnotation}. See {@link EMLAnnotation#defaults} for
          * details on what attributes can be passed to the EMLAnnotation. In addition, there is an `elementName` property.
          * @property {string} [annotationData.elementName] The name of the EML Element that this
          annotation should be applied to. e.g. dataset, entity, attribute. Defaults to `dataset`. NOTE: Right now only dataset annotations are supported until
          more annotation editing is added to the EML Editor.
          * @property {Boolean} [annotationData.allowDuplicates] If false, this annotation will replace all annotations already set with the same propertyURI.
          * By default, more than one annotation with a given propertyURI can be added (defaults to true)
          */
          addAnnotation: function(annotationData){

            try{
              if( !annotationData || typeof annotationData != "object" ){
                return;
              }

              //If no element name is provided, default to the dataset element.
              let elementName = "";
              if( !annotationData.elementName ){
                elementName = "dataset"
              }
              else{
                elementName = annotationData.elementName;
              }
              //Remove the elementName property so it isn't set on the EMLAnnotation model later.
              delete annotationData.elementName;

              //Check if duplicates are allowed
              let allowDuplicates = annotationData.allowDuplicates;
              delete annotationData.allowDuplicates;

              //Create a new EMLAnnotation model
              let annotation = new EMLAnnotation(annotationData);

              //Update annotations set on the dataset element
              if( elementName == "dataset" ){
                let annotations = this.get("annotations");

                //If the current annotations set on the EML model are not in Array form, change it to an array
                if( !annotations ){
                  annotations = new EMLAnnotations();
                }

                if( allowDuplicates === false ){
                  //Add the EMLAnnotation to the collection, making sure to remove duplicates first
                  annotations.replaceDuplicateWith(annotation);
                }
                else{
                  annotations.add(annotation);
                }

                //Set the annotations and force the change to be recognized by the model
                this.set("annotations", annotations, {silent: true});
                this.handleChange(this, { force: true });

              }
              else{
                /** @todo Add annotation support for other EML Elements */
              }

            }
            catch(e){
              console.error("Could not add Annotation to the EML: ", e);
            }

          },

          /**
          * Finds annotations that are of the `data sensitivity` property from the NCEAS SENSO ontology.
          * Returns undefined if none are found. This function returns EMLAnnotation models because the data
          * sensitivity is stored in the EML Model as EMLAnnotations and added to EML as semantic annotations.
          * @returns {EMLAnnotation[]|undefined}
          */
          getDataSensitivity: function(){
            try{
              let annotations = this.get("annotations");
              if(annotations){
                let found = annotations.where({ propertyURI: this.get("dataSensitivityPropertyURI") });
                if( !found || !found.length ){
                  return;
                }
                else{
                  return found;
                }
              }
              else{
                return;
              }
            }
            catch(e){
              console.error("Failed to get Data Sensitivity from EML model: ", e);
              return;
            }
          }

        });

        return EML211;
      }
    );