API Docs for: 0.0.1
Show:

File: lib/serialize/caaml5.js

/*
 * niViz -- snow profiles visualization
 * Copyright (C) 2015 WSL/SLF - Fluelastrasse 11 - 7260 Davos Dorf - Switzerland.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/* eslint no-loop-func: [0], complexity: [2, 30] */
(function (niviz) {
  'use strict';

  // --- Module Dependencies ---
  var Header = niviz.Header;
  var round  = niviz.util.round;

  /** @module niviz */

  /**
   * Serialize data into CAAML format
   *
   * @class CAAML
   * @constructor
   */
  var CAAML = {};

  CAAML.precipiType = [
    '-DZ', 'DZ', '+DZ', '-RA', 'RA', '+RA', '-SN', 'SN', '+SN', '-SG', 'SG', '+SG',
    '-IC', 'IC', '+IC', '-PE', 'PE', '+PE', '-GR', 'GR', '+GR', '-GS', 'GS', '+GS',
    'UP', 'Nil', 'RASN', 'FZRA'
  ];

  CAAML.skyconditions = ['CLR', 'FEW', 'SCT', 'BKN', 'OVC', 'X'];

  CAAML.NS = {
    caaml: 'http://caaml.org/Schemas/V5.0/Profiles/SnowProfileIACS',
    xs: 'http://www.w3.org/2001/XMLSchema'
  };

  CAAML.addNS = function (node, profile) {
    node.setAttribute('xmlns:gml', 'http://www.opengis.net/gml');
    node.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
    node.setAttribute('gml:id', profile.info.profile_gmlid
                      || niviz.CAAML.defaults.profile_gmlid || 'NA');
  };

  CAAML.addMetaData = function (station, profile, dom, node) {
    var mp = node.appendChild(dom.createElement('caaml:metaDataProperty'));
    var md = mp.appendChild((dom.createElement('caaml:MetaData')));

    var dr = md.appendChild((dom.createElement('caaml:dateTimeReport')));
    dr.textContent = moment().format('YYYY-MM-DDTHH:mm:ss.sssZ');

    var sr = md.appendChild(dom.createElement('caaml:srcRef')), pe, na, op;

    if (!profile.info.operation) {
      pe = sr.appendChild((dom.createElement('caaml:Person')));
      pe.setAttribute('gml:id', profile.info.observer_gmlid
                      || niviz.CAAML.defaults.observer_gmlid || 'NA');

      na = pe.appendChild((dom.createElement('caaml:name')));
      na.textContent = profile.info.observer || '';
    } else {
      op = sr.appendChild((dom.createElement('caaml:Operation')));
      op.setAttribute('gml:id', profile.info.operation_gmlid
                      || niviz.CAAML.defaults.operation_gmlid || 'NA');

      na = op.appendChild((dom.createElement('caaml:name')));
      na.textContent = profile.info.operation || '';

      var cp = op.appendChild((dom.createElement('caaml:contactPerson')));

      pe = cp.appendChild((dom.createElement('caaml:Person')));
      pe.setAttribute('gml:id', profile.info.observer_gmlid
                      || niviz.CAAML.defaults.observer_gmlid || 'NA');

      na = pe.appendChild((dom.createElement('caaml:name')));
      na.textContent = profile.info.observer || '';
    }

    if (profile.info.comment && profile.info.comment.metadata)
      CAAML.addNode(dom, md, profile.info.comment.metadata, 'caaml:comment');

    if (profile.info.custom_meta) // looping through customData
      md.appendChild(profile.info.custom_meta);
  };

  CAAML.addValidTime = function (profile, gmlid, dom, node) {
    var vt = node.appendChild(dom.createElement('caaml:validTime'));
    var ti = vt.appendChild((dom.createElement('caaml:TimeInstant')));
    ti.setAttribute('gml:id', gmlid);

    var tp = ti.appendChild(dom.createElement('caaml:timePosition'));
    tp.textContent = profile.date.format('YYYY-MM-DDTHH:mm:ss.sssZ');
  };

  CAAML.addNode = function (dom, node, parameter, cname, uom, decimals) {
    if (parameter !== undefined && parameter !== null) {
      var tmp = node.appendChild(dom.createElement(cname));
      if (uom !== undefined) tmp.setAttribute('uom', uom);

      if (decimals === undefined)
        tmp.textContent = parameter;
      else
        tmp.textContent = parameter.toFixed(decimals);
    }
  };

  CAAML.addPointProfile = function (dom, profile, basenode, feature, attributes, nodes) {
    var mc = basenode.appendChild(dom.createElement('caaml:MeasurementComponents')), i, l;

    attributes.forEach(function (attribute) {
      mc.setAttribute(attribute[0], attribute[1]);
    });

    nodes.forEach(function (node) {
      CAAML.addNode(dom, mc, 'template', node);
    });

    var m = basenode.appendChild(dom.createElement('caaml:Measurements'));

    var ntuples = [];
    for (i = feature.layers.length - 1; i >= 0; --i) {
      l = feature.layers[i];
      ntuples.push(round(profile.top - l.top, 10) + ',' + round(l.value, 10));
    }

    CAAML.addNode(dom, m, ntuples.join(' '), 'caaml:tupleList');
  };

  CAAML.addSnowProfile = function (station, profile, dom, root) {
    var sp = root.appendChild(dom.createElement('caaml:snowProfileResultsOf'));
    var sm = sp.appendChild(dom.createElement('caaml:SnowProfileMeasurements'));
    sm.setAttribute('dir', 'top down');
    if (profile.info.custom_snow) // looping through customData
      sm.appendChild(profile.info.custom_snow);

    if (profile.info.comment && profile.info.comment.weather)
      CAAML.addNode(dom, sm, profile.info.comment.weather, 'caaml:comment');

    if (profile.height || profile.height === 0)
      CAAML.addNode(dom, sm, profile.height, 'caaml:profileDepth', 'cm');
    if (profile.info.sky !== '')
      CAAML.addNode(dom, sm, profile.info.sky, 'caaml:skyCond');
    if (profile.info.precipitation)
      CAAML.addNode(dom, sm, profile.info.precipitation, 'caaml:precipTI');
    if (profile.info.ta || profile.info.ta === 0)
      CAAML.addNode(dom, sm, profile.info.ta, 'caaml:airTempPres', 'degC');

    if (profile.info.wind) {
      if (profile.info.wind.speed)
        CAAML.addNode(dom, sm, profile.info.wind.speed, 'caaml:windSpd', 'ms-1');

      if (profile.info.wind.dir) {
        var dir = sm.appendChild(dom.createElement('caaml:windDir'));
        var aspect = dir.appendChild(dom.createElement('caaml:AspectPosition'));
        CAAML.addNode(dom, aspect, profile.info.wind.dir, 'caaml:position');
      }
    }

    if (profile.hs || profile.hs === 0 || (profile.swe && profile.density)) {
      var hs = sm.appendChild(dom.createElement('caaml:hS'));
      var co = hs.appendChild(dom.createElement('caaml:Components'));

      if (profile.info.hs === 0) {
        // do nothing, outputting anything would be confusing
      } else if (profile.hs && (!profile.density || !profile.density.height
                                || profile.hs !== profile.density.height)) {
        // this is an interesting case: it means that we that either
        // density measurement and hs are out of sync or that we have a
        // profile with soil; since swe is anyway calculated internally
        // we have a preference for hs, it's more consistent
        CAAML.addNode(dom, co, profile.hs, 'caaml:snowHeight', 'cm');

        // Only output if hs not more than 10cm off density height
        if (Header.checkdensity(profile))
          CAAML.addNode(dom, co,
            Math.round(profile.density.elements[0].average() * profile.hs / 100),
            'caaml:swe', 'mm');
      } else if (profile.swe && profile.density) {
        CAAML.addNode(dom, co, profile.density.height, 'caaml:snowHeight', 'cm');
        CAAML.addNode(dom, co, profile.swe, 'caaml:swe', 'mm');
      } else {
        CAAML.addNode(dom, co, profile.hs, 'caaml:snowHeight', 'cm');
      }
    }

    if (profile.info && (profile.info.hn24 || profile.info.hn24 === 0)) {
      var hn24 = sm.appendChild(dom.createElement('caaml:hN24'));
      var com = hn24.appendChild(dom.createElement('caaml:Components'));
      CAAML.addNode(dom, com, profile.info.hn24, 'caaml:snowHeight', 'cm');
    }

    if (profile.info.penetration) {
      if (profile.info.penetration.ram !== '')
        CAAML.addNode(dom, sm, profile.info.penetration.ram, 'caaml:penetrationRam', 'cm');
      if (profile.info.penetration.foot !== '')
        CAAML.addNode(dom, sm, profile.info.penetration.foot, 'caaml:penetrationFoot', 'cm');
      if (profile.info.penetration.ski !== '')
        CAAML.addNode(dom, sm, profile.info.penetration.ski, 'caaml:penetrationSki', 'cm');
    }

    if (profile.info.roughness) {
      var sf = sm.appendChild(dom.createElement('caaml:surfFeatures'));
      var sfc = sf.appendChild(dom.createElement('caaml:Components'));
      CAAML.addNode(dom, sfc, profile.info.roughness, 'caaml:surfRoughness');
    }

    CAAML.addStratProfile(profile, dom, sm);
    CAAML.addTempProfile(profile, dom, sm);
    CAAML.addDensityProfile(profile, dom, sm);

    if (profile.wetness && profile.wetness.length > 1)
      CAAML.addLwcProfile(profile, dom, sm, profile.wetness.elements[1]);

    if (profile.ssa && profile.ssa.length > 1)
      CAAML.addSSAProfile(profile, dom, sm, profile.ssa.elements[1]);

    profile.ramm && profile.ramm.elements.forEach(function (ram) {
      CAAML.addHardnessProfile(profile, dom, sm, ram);
    });

    profile.smp && profile.smp.elements.forEach(function (smp) {
      CAAML.addSMPProfile(profile, dom, sm, smp);
    });

    CAAML.addStabilityProfile(profile, dom, sm);
  };

  CAAML.addStratProfile = function (profile, dom, node) {

    var stratigraphic = [ 'grainshape', 'grainsize', 'hardness', 'wetness' ],
      param;

    stratigraphic.forEach(function (parameter) {
      if (profile[parameter] && profile[parameter].elements[0] &&
          profile[parameter].elements[0].layers.length) {
        param = parameter;
      }
    });

    if (!param) return;

    var sp = node.appendChild(dom.createElement('caaml:stratProfile')),
      i = profile[param].layers.length - 1, j = 0;

    for ( ; i >= 0; --i) {
      ++j;

      var hardness, wetness, gs1, gs2, gsavg, gsmax;

      var l = sp.appendChild(dom.createElement('caaml:Layer'));

      var comment = profile.comments && profile.comments.layers[i].value;
      if (comment) CAAML.addNode(dom, l, comment, 'caaml:comment');

      var depth = round(profile.top - profile[param].layers[i].top, 4);
      var thickness = round(profile[param].layers[i].top - profile[param].layers[i].bottom, 4);

      hardness = wetness = gs1 = gs2 = gsavg = gsmax = null;
      var hlen = profile.hardness && profile.hardness.layers.length,
        wlen = profile.wetness && profile.wetness.elements[0] && profile.wetness.layers.length,
        gflen = profile.grainshape && profile.grainshape.layers.length,
        gslen = profile.grainsize && profile.grainsize.layers.length;

      if (profile.hardness && profile.hardness.layers[hlen - j])
        hardness = profile.hardness.layers[hlen - j].caamlcode;

      if (profile.wetness && profile.wetness.elements[0] && profile.wetness.layers[wlen - j])
        wetness = profile.wetness.layers[wlen - j].code;

      if (profile.grainshape && profile.grainshape.layers[gflen - j]) {
        gs1 = profile.grainshape.layers[gflen - j].value.primary;
        gs2 = profile.grainshape.layers[gflen - j].value.secondary;
      }

      if (profile.grainsize && profile.grainsize.layers[gslen - j]
          && profile.grainsize.layers[gslen - j].value) {
        gsavg = profile.grainsize.layers[gslen - j].value.avg;
        gsmax = profile.grainsize.layers[gslen - j].value.max;
      }

      CAAML.addNode(dom, l, depth, 'caaml:depthTop', 'cm');
      CAAML.addNode(dom, l, thickness, 'caaml:thickness', 'cm');

      if (gs1) CAAML.addNode(dom, l, gs1, 'caaml:grainFormPrimary');
      if (gs1 && gs2) CAAML.addNode(dom, l, gs2, 'caaml:grainFormSecondary');

      if (gsavg || gsmax) {
        var gs = l.appendChild(dom.createElement('caaml:grainSize'));
        gs.setAttribute('uom', 'mm');

        var co = gs.appendChild(dom.createElement('caaml:Components'));
        if (gsavg) CAAML.addNode(dom, co, gsavg, 'caaml:avg');
        if (gsmax) CAAML.addNode(dom, co, gsmax, 'caaml:avgMax');
      }

      if (hardness) CAAML.addNode(dom, l, hardness, 'caaml:hardness', 'N');
      if (wetness) CAAML.addNode(dom, l, wetness, 'caaml:lwc', '');
    }
  };

  CAAML.addTempProfile = function (profile, dom, node) {
    if (!profile.temperature) return;

    var tp = node.appendChild(dom.createElement('caaml:tempProfile')), i;
    tp.setAttribute('uomDepth', 'cm');
    tp.setAttribute('uomTemp', 'degC');

    if (profile.temperature.info.comment) {
      var tpmd = tp.appendChild(dom.createElement('caaml:MetaData'));
      CAAML.addNode(dom, tpmd, profile.temperature.info.comment, 'caaml:comment');
    }

    for (i = profile.temperature.layers.length - 1; i >= 0; --i) {
      var l = profile.temperature.layers[i];

      // Skip null values
      if (!l.value && l.value !== 0) continue;

      var ob = tp.appendChild(dom.createElement('caaml:Obs'));
      CAAML.addNode(dom, ob, round(profile.top - l.top, 4), 'caaml:depth');
      CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:snowTemp');
    }
  };

  CAAML.addDensityProfile = function (profile, dom, node) {
    if (!profile.density) return;

    var tp = node.appendChild(dom.createElement('caaml:densityProfile')), i;
    tp.setAttribute('uomDepthTop', 'cm');
    tp.setAttribute('uomDensity', 'kgm-3');
    tp.setAttribute('uomThickness', 'cm');

    if (profile.density.info.comment) {
      var tpmd = tp.appendChild(dom.createElement('caaml:MetaData'));
      CAAML.addNode(dom, tpmd, profile.density.info.comment, 'caaml:comment');
    }

    for (i = profile.density.layers.length - 1; i >= 0; --i) {
      var l = profile.density.layers[i];

      // Skip null values
      if (!l.value && l.value !== 0) continue;

      var ob = tp.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, ob, round(profile.top - l.top, 4), 'caaml:depthTop');
      CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness');
      CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:density');
    }
  };

  CAAML.addHardnessProfile = function (profile, dom, node, feature) {
    if (!feature) return;

    var hp = node.appendChild(dom.createElement('caaml:hardnessProfile')), i;
    hp.setAttribute('uomDepthTop', 'cm');
    hp.setAttribute('uomThickness', 'cm');
    hp.setAttribute('uomDropHeight', 'cm');
    hp.setAttribute('uomHardness', 'N');
    hp.setAttribute('uomWeightHammer', 'kg');
    hp.setAttribute('uomWeightTube', 'kg');

    var md = hp.appendChild(dom.createElement('caaml:MetaData'));
    if (feature.info.comment)
      CAAML.addNode(dom, md, feature.info.comment, 'caaml:comment');

    CAAML.addNode(dom, md, 'Ram Sonde', 'caaml:methodOfMeas');

    for (i = feature.layers.length - 1; i >= 0; --i) {
      var l = feature.layers[i];

      var ob = hp.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, ob, round(profile.top - l.top, 4), 'caaml:depthTop');
      CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness');
      CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:hardness');

      CAAML.addNode(dom, ob, l.weightHammer, 'caaml:weightHammer');
      CAAML.addNode(dom, ob, l.weightTube, 'caaml:weightTube');
      CAAML.addNode(dom, ob, l.nDrops, 'caaml:nDrops');
      CAAML.addNode(dom, ob, l.dropHeight, 'caaml:dropHeight');
    }
  };

  CAAML.addSMPProfile = function (profile, dom, node, feature) {
    if (!feature || !feature.layers || !feature.layers.length) return;

    var hp = node.appendChild(dom.createElement('caaml:hardnessProfile'));
    hp.setAttribute('uomDepthTop', 'cm');
    hp.setAttribute('uomThickness', 'cm');
    hp.setAttribute('uomHardness', 'N');

    var md = hp.appendChild(dom.createElement('caaml:MetaData'));
    if (feature.info.comment)
      CAAML.addNode(dom, md, feature.info.comment, 'caaml:comment');

    CAAML.addNode(dom, md, 'Snow Micro Pen', 'caaml:methodOfMeas');

    if (feature.info.surf || feature.info.surf === 0)
      CAAML.addNode(dom, md, feature.info.surf, 'caaml:surfOfIndentation', 'm2');

    CAAML.addPointProfile(
      dom, profile, hp, feature, [],
      ['caaml:depth', 'caaml:penRes']
    );
  };

  CAAML.addLwcProfile = function (profile, dom, node, feature) {
    if (!feature) return;

    var lp = node.appendChild(dom.createElement('caaml:lwcProfile')), i;
    lp.setAttribute('uomDepthTop', 'cm');
    lp.setAttribute('uomThickness', 'cm');
    lp.setAttribute('uomLwc', '');

    if (feature.info.comment) {
      var lpmd = lp.appendChild(dom.createElement('caaml:MetaData'));
      CAAML.addNode(dom, lpmd, feature.info.comment, 'caaml:comment');
    }

    for (i = feature.layers.length - 1; i >= 0; --i) {
      var l = feature.layers[i];

      // Skip null values
      if (!l.value && l.value !== 0) continue;

      var ob = lp.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, ob, round(profile.top - l.top, 4), 'caaml:depthTop');
      CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness');
      CAAML.addNode(dom, ob, l.code, 'caaml:lwc');
    }
  };

  CAAML.addSSAProfile = function (profile, dom, node, feature) {
    if (!feature) return;

    var sp = node.appendChild(dom.createElement('caaml:specSurfAreaProfile')), i;
    sp.setAttribute('uomDepthTop', 'cm');
    sp.setAttribute('uomThickness', 'cm');
    sp.setAttribute('uomSpecSurfArea', 'm2kg-1');

    var spmd = sp.appendChild(dom.createElement('caaml:MetaData'));
    if (feature.info.comment)
      CAAML.addNode(dom, spmd, feature.info.comment, 'caaml:comment');

    CAAML.addNode(dom, spmd, feature.info.method || 'other', 'caaml:methodOfMeas');

    if (feature.info.pointprofile) {
      CAAML.addPointProfile(
        dom, profile, sp, feature, [],
        ['caaml:depth', 'caaml:specSurfArea']);

      return;
    }

    for (i = feature.layers.length - 1; i >= 0; --i) {
      var l = feature.layers[i];

      // Skip null values
      if (!l.value && l.value !== 0) continue;

      var ob = sp.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, ob, round(profile.top - l.top, 4), 'caaml:depthTop');
      CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness');
      CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:specSurfArea');
    }
  };

  CAAML.addStabilityProfile = function (profile, dom, node) {
    if (!profile.ct && !profile.rb && !profile.ect && !profile.saw && !profile.sf) return;

    var st = node.appendChild(dom.createElement('caaml:stbTests'));

    if (profile.ct) CAAML.addCT(profile, dom, st);
    if (profile.ect) CAAML.addECT(profile, dom, st);
    if (profile.rb) CAAML.addRB(profile, dom, st);
    if (profile.sf) CAAML.addSF(profile, dom, st);
    if (profile.saw) CAAML.addSAW(profile, dom, st);
  };

  CAAML.addCT = function (profile, dom, node) {
    for (var i = profile.ct.layers.length - 1; i >= 0; --i) {
      var l = profile.ct.layers[i];
      var ct = node.appendChild(dom.createElement('caaml:ComprTest'));

      if (l.nofailure) {
        ct.appendChild(dom.createElement('caaml:noFailure'));
        continue;
      }

      if (l.comment) CAAML.addNode(dom, ct, l.comment, 'caaml:comment');
      var fo = ct.appendChild(dom.createElement('caaml:failedOn'));

      var cl = fo.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, cl, round(profile.top - l.top, 4), 'caaml:depthTop', 'cm');

      var re = fo.appendChild(dom.createElement('caaml:Results'));
      if (l.character) CAAML.addNode(dom, re, l.character, 'caaml:fractureCharacter');
      CAAML.addNode(dom, re, l.value ? Math.round(l.value) : 'CTV', 'caaml:testScore');
    }
  };

  CAAML.addECT = function (profile, dom, node) {
    for (var i = profile.ect.layers.length - 1; i >= 0; --i) {
      var l = profile.ect.layers[i];
      var ect = node.appendChild(dom.createElement('caaml:ExtColumnTest'));

      if (l.nofailure) {
        ect.appendChild(dom.createElement('caaml:noFailure'));
        continue;
      }

      if (l.comment) CAAML.addNode(dom, ect, l.comment, 'caaml:comment');
      var fo = ect.appendChild(dom.createElement('caaml:failedOn'));

      var el = fo.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, el, round(profile.top - l.top, 4), 'caaml:depthTop', 'cm');

      var re = fo.appendChild(dom.createElement('caaml:Results'));
      CAAML.addNode(dom, re, l.text, 'caaml:testScore');
    }
  };

  CAAML.addRB = function (profile, dom, node) {
    for (var i = profile.rb.layers.length - 1; i >= 0; --i) {
      var l = profile.rb.layers[i];
      var rb = node.appendChild(dom.createElement('caaml:RBlockTest'));

      if (l.nofailure) {
        rb.appendChild(dom.createElement('caaml:noFailure'));
        continue;
      }

      if (l.comment) CAAML.addNode(dom, rb, l.comment, 'caaml:comment');
      var fo = rb.appendChild(dom.createElement('caaml:failedOn'));

      var cl = fo.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, cl, round(profile.top - l.top, 4), 'caaml:depthTop', 'cm');

      var re = fo.appendChild(dom.createElement('caaml:Results'));
      if (l.character) CAAML.addNode(dom, re, l.character, 'caaml:fractureCharacter');
      if (l.releasetype) CAAML.addNode(dom, re, l.releasetype, 'caaml:releaseType');
      CAAML.addNode(dom, re, l.text, 'caaml:testScore');
    }
  };

  CAAML.addSF = function (profile, dom, node) {
    for (var i = profile.sf.layers.length - 1; i >= 0; --i) {
      var l = profile.sf.layers[i];
      var sf = node.appendChild(dom.createElement('caaml:ShearFrameTest'));

      if (l.nofailure) {
        sf.appendChild(dom.createElement('caaml:noFailure'));
        continue;
      }

      if (l.comment) CAAML.addNode(dom, sf, l.comment, 'caaml:comment');
      var fo = sf.appendChild(dom.createElement('caaml:failedOn'));

      var cl = fo.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, cl, round(profile.top - l.top, 4), 'caaml:depthTop', 'cm');

      var re = fo.appendChild(dom.createElement('caaml:Results'));
      if (l.character) CAAML.addNode(dom, re, l.character, 'caaml:fractureCharacter');
      CAAML.addNode(dom, re, l.value, 'caaml:failureForce', 'N');
    }
  };

  CAAML.addSAW = function (profile, dom, node) {
    for (var i = profile.saw.layers.length - 1; i >= 0; --i) {
      var l = profile.saw.layers[i];
      var saw = node.appendChild(dom.createElement('caaml:PropSawTest'));
      var fo = saw.appendChild(dom.createElement('caaml:failedOn'));

      var cl = fo.appendChild(dom.createElement('caaml:Layer'));
      CAAML.addNode(dom, cl, round(profile.top - l.top, 4), 'caaml:depthTop', 'cm');

      var re = fo.appendChild(dom.createElement('caaml:Results'));

      CAAML.addNode(dom, re, l.text, 'caaml:fracturePropagation');
      CAAML.addNode(dom, re, l.cutlength, 'caaml:cutLength', 'cm');
      CAAML.addNode(dom, re, l.columnlength, 'caaml:columnLength', 'cm');
    }
  };

  CAAML.addLocRef = function (station, profile, dom, root) {
    var lr = root.appendChild(dom.createElement('caaml:locRef'));
    var op = lr.appendChild(dom.createElement('caaml:ObsPoint'));
    op.setAttribute('gml:id', station.id || niviz.CAAML.defaults.station_gmlid || 'NA');

    if (profile.info.custom_loc) { // looping through customData
      var mp = op.appendChild(dom.createElement('caaml:metaDataProperty'));
      var md = mp.appendChild(dom.createElement('caaml:MetaData'));
      md.appendChild(profile.info.custom_loc);
    }

    if (station.position && station.position.description)
      CAAML.addNode(dom, op, station.position.description, 'caaml:description');

    CAAML.addNode(dom, op, station.name, 'caaml:name');
    CAAML.addNode(dom, op, station.position.subtype || '', 'caaml:obsPointSubType');

    if (station.position.altitude) {
      var vh = op.appendChild(dom.createElement('caaml:validElevation'));
      var ep = vh.appendChild(dom.createElement('caaml:ElevationPosition'));
      ep.setAttribute('uom', 'm');

      var alt = station.position.altitude;
      if (alt || alt === 0) alt = Math.round(alt);

      CAAML.addNode(dom, ep, alt, 'caaml:position');
    }

    if (station.position.aspect) {
      var va = op.appendChild(dom.createElement('caaml:validAspect'));
      var ap = va.appendChild(dom.createElement('caaml:AspectPosition'));

      if (station.position.azimuth || station.position.azimuth === 0) {
        var azi = station.position.azimuth;
        if (!isNaN(azi)) azi = Math.round(azi);
        CAAML.addNode(dom, ap, azi, 'caaml:position');
      } else {
        var aspect = station.position.aspect;
        if (!isNaN(aspect)) aspect = Math.round(aspect);
        CAAML.addNode(dom, ap, aspect, 'caaml:position');
      }
    }

    if (station.position.angle !== undefined && station.position.angle !== null
        && station.position.direction !== 'flat') {
      var vs = op.appendChild(dom.createElement('caaml:validSlopeAngle'));
      var sp = vs.appendChild(dom.createElement('caaml:SlopeAnglePosition'));
      sp.setAttribute('uom', 'deg');

      var angle = station.position.angle;
      if (!isNaN(angle)) angle = Math.round(angle);

      CAAML.addNode(dom, sp, angle, 'caaml:position');
    }

    if (station.position.latitude !== undefined && station.position.longitude !== undefined) {
      var pl = op.appendChild(dom.createElement('caaml:pointLocation'));
      var po = pl.appendChild(dom.createElement('gml:Point'));
      po.setAttribute('gml:id', 'POINT_ID');
      po.setAttribute('srsName', 'urn:ogc:def:crs:OGC:1.3:CRS84');
      po.setAttribute('srsDimension', '2');

      var tx = station.position.longitude + ' ' + station.position.latitude;
      CAAML.addNode(dom, po, tx, 'gml:pos');
    }
  };

  /**
   * Serialize Graph object
   *
   * @method serialize
   * @static
   *
   * @param {Object} data The profile graph, e.g. SimpleProfile, SLFProfile.
   * @returns {String} The serialized result.
   */
  CAAML.serialize = function (data) {
    var result = '<?xml version="1.0" encoding="UTF-8"?>\n', serializer = new XMLSerializer();
    var station = data.station, profile = data.profile;
    var dom = document.implementation.createDocument('', '', null);
    var root = dom.appendChild(dom.createElementNS(CAAML.NS.caaml, 'caaml:SnowProfile'));
    var gmlid = station.id || station.name || 'NA';
    gmlid.replace(' ', '');

    CAAML.addNS(root, profile);
    CAAML.addMetaData(station, profile, dom, root);
    CAAML.addValidTime(profile, gmlid + '_3', dom, root);
    CAAML.addSnowProfile(station, profile, dom, root);
    CAAML.addLocRef(station, profile, dom, root);

    result += serializer.serializeToString(root);
    result = format(result);
    return result;
  };

  function format (xml) {
    var formatted = '', reg = /(>)(<)(\/*)/g, pad = 0, indent, padding;
    xml = xml.replace(reg, '$1\r\n$2$3');

    xml.split('\r\n').forEach(function(node, index) {
      indent = 0;

      if (node.match( /.+<\/\w[^>]*>$/ )) {
        indent = 0;
      } else if (node.match( /^<\/\w/ )) {
        if (pad !== 0) {
          pad -= 1;
        }
      } else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) {
        indent = 1;
      } else {
        indent = 0;
      }

      padding = '';
      for (var i = 0; i < pad; i++) padding += '  ';

      formatted += padding + node + '\r\n';
      pad += indent;
    });

    return formatted;
  }

  // --- Module Exports ---
  niviz.CAAML5 = CAAML;

}(niviz));