API Docs for: 0.0.1
Show:

File: lib/serialize/caaml.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 Config = niviz.Config;
  var Header = niviz.Header;
  var round  = niviz.util.round;

  /** @module niviz */

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

  /**
   * The general settings.
   *
   * @property defaults
   * @type Array<Object>
   * @static
   */
  CAAML.defaults = new Config('CAAML', [
    {
      name: 'profile_gmlid', type: 'string', default: 'niviz_profile'
    },
    {
      name: 'station_gmlid', type: 'string', default: 'niviz_station'
    },
    {
      name: 'operation_gmlid', type: 'string', default: 'niviz_operation'
    },
    {
      name: 'observer_gmlid', type: 'string', default: 'niviz_observer'
    }
  ]);

  CAAML.defaults.load();

  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.roughness = [
    { id: 'rsm', symbol: 'U', name: 'smooth' },
    { id: 'rwa', symbol: 'V', name: 'wavy' },
    { id: 'rcv', symbol: 'W', name: 'concave furrows' },
    { id: 'rcx', symbol: 'X', name: 'convex furrows' },
    { id: 'rrd', symbol: 'Y', name: 'random furrows' }
  ];

  CAAML.NS = {
    caaml: 'http://caaml.org/Schemas/SnowProfileIACS/v6.0.3',
    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 md = node.appendChild((dom.createElement('caaml:metaData')));

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

  CAAML.addTimeRef = function (profile, dom, node) {
    var tr = node.appendChild(dom.createElement('caaml:timeRef'));

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

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

  CAAML.addSrcRef = function (profile, dom, node) {
    var sr = node.appendChild((dom.createElement('caaml:srcRef')));
    var 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')));

      cp.setAttribute('gml:id', profile.info.observer_gmlid
                      || niviz.CAAML.defaults.observer_gmlid || 'NA');

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

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

    var md = lr.appendChild((dom.createElement('caaml:metaData')));
    if (station.position && station.position.description)
      CAAML.addNode(dom, md, station.position.description, 'caaml:comment');

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

    if (station.position.altitude) {
      var vh = lr.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 = lr.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 || station.position.angle === 0)
        && station.position.direction !== 'flat') {
      var vs = lr.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 || station.position.latitude === 0)
        && (station.position.longitude || station.position.longitude === 0)) {
      var pl = lr.appendChild(dom.createElement('caaml:pointLocation'));
      var po = pl.appendChild(dom.createElement('gml:Point'));
      po.setAttribute('gml:id', 'pointID');
      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');
    }
  };

  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');

    var md = sm.appendChild((dom.createElement('caaml:metaData')));
    if (profile.info.comment && profile.info.comment.SnowProfileMeasurements)
      CAAML.addNode(dom, md, profile.info.comment.SnowProfileMeasurements, 'caaml:comment');

    if (profile.height || profile.height === 0)
      CAAML.addNode(dom, sm, profile.height, 'caaml:profileDepth', 'cm');

    var wc = sm.appendChild(dom.createElement('caaml:weatherCond'));
    md = wc.appendChild(dom.createElement('caaml:metaData'));

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

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

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

      if (profile.info.wind.dir || profile.info.wind.angle || profile.info.wind.angle === 0) {
        var dir = wc.appendChild(dom.createElement('caaml:windDir'));
        var aspect = dir.appendChild(dom.createElement('caaml:AspectPosition'));

        var angle = profile.info.wind.angle;
        if (!isNaN(angle)) angle = Math.round(angle);

        CAAML.addNode(dom, aspect, angle || profile.info.wind.dir, 'caaml:position');
      }
    }

    var spc = sm.appendChild((dom.createElement('caaml:snowPackCond')));

    if (profile.info.comment && profile.info.comment.snowpack) {
      md = spc.appendChild(dom.createElement('caaml:metaData'));
      CAAML.addNode(dom, md, profile.info.comment.snowpack, 'caaml:comment');
    }

    if (profile.hs || profile.hs === 0 || (profile.swe && profile.density)) {
      var hs = spc.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:height', '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:waterEquivalent', 'kgm-2');
      } else if (profile.swe && profile.density) {
        CAAML.addNode(dom, co, profile.density.height, 'caaml:height', 'cm');
        CAAML.addNode(dom, co, profile.swe, 'caaml:waterEquivalent', 'kgm-2');
      } else {
        CAAML.addNode(dom, co, profile.hs, 'caaml:height', 'cm');
      }
    }

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

    var sc = sm.appendChild((dom.createElement('caaml:surfCond')));
    md = sc.appendChild((dom.createElement('caaml:metaData')));
    if (profile.info.comment && profile.info.comment.surface)
      CAAML.addNode(dom, md, profile.info.comment.surface, 'caaml:comment');

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

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

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

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

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

    CAAML.addImpurityProfile(profile, dom, sm);

    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;

    sp.appendChild(dom.createElement('caaml:stratMetaData'));

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

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

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

      var comment = profile.comments && profile.comments.layers[i].value;
      if (comment) CAAML.addNode(dom, md, 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', '');
      if (wetness) CAAML.addNode(dom, l, wetness, 'caaml:wetness', '');
    }
  };

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

    var tp = node.appendChild(dom.createElement('caaml:tempProfile')), i;
    var tpmd = tp.appendChild(dom.createElement('caaml:tempMetaData'));

    if (profile.temperature.info.comment)
      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', 'cm');
      CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:snowTemp', 'degC');
    }
  };

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

    profile.density.elements.forEach(function (density) {
      var tp = node.appendChild(dom.createElement('caaml:densityProfile')), i;
      var tpmd = tp.appendChild(dom.createElement('caaml:densityMetaData'));
      if (density.info.comment)
        CAAML.addNode(dom, tpmd, density.info.comment, 'caaml:comment');

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

      for (i = density.layers.length - 1; i >= 0; --i) {
        var l = 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', 'cm');
        CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness', 'cm');
        CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:density', 'kgm-3');
      }
    });
  };

  CAAML.addImpurityProfile = function (profile, dom, node) {
    if (!profile.impurity || !profile.impurity.elements.length) return;

    profile.impurity.elements.forEach(function (impurity) {
      var tp = node.appendChild(dom.createElement('caaml:impurityProfile')), i;
      var tpmd = tp.appendChild(dom.createElement('caaml:impurityMetaData'));
      if (impurity.info.comment)
        CAAML.addNode(dom, tpmd, impurity.info.comment, 'caaml:comment');

      CAAML.addNode(dom, tpmd, impurity.info.impuritytype || 'other', 'caaml:impurity');
      CAAML.addNode(dom, tpmd, impurity.info.method || 'other', 'caaml:methodOfMeas');

      var fractiontype = impurity.info.fractiontype;

      for (i = impurity.layers.length - 1; i >= 0; --i) {
        var l = impurity.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', 'cm');
        CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness', 'cm');
        CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:' + fractiontype, '%');
      }
    });
  };

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

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

    var md = hp.appendChild(dom.createElement('caaml:hardnessMetaData'));
    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', 'cm');
      CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness', 'cm');
      CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:hardness', 'N');

      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'));

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

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

    if (feature.info.uncertainty || feature.info.uncertainty === 0)
      CAAML.addNode(dom, md, feature.info.uncertainty, 'caaml:uncertaintyOfMeas', 'N');

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

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

  CAAML.addLwcProfile = function (profile, dom, node) {
    if (!profile.wetness || profile.wetness.elements.length <= 1) return;

    var i, j;
    for (j = 1; j < profile.wetness.elements.length; ++j) {
      var lp = node.appendChild(dom.createElement('caaml:lwcProfile')),
        feature = profile.wetness.elements[j];

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

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

      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', 'cm');
        CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness', 'cm');
        CAAML.addNode(dom, ob, l.value, 'caaml:lwc', '% by Vol');
      }
    }
  };

  CAAML.addSSAProfile = function (profile, dom, node) {
    if (!profile.ssa || !profile.ssa.elements.length) return;

    profile.ssa.elements.forEach(function (feature) {
      var sp = node.appendChild(dom.createElement('caaml:specSurfAreaProfile')), i, l;

      var spmd = sp.appendChild(dom.createElement('caaml:specSurfAreaMetaData'));
      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.uncertainty || feature.info.uncertainty === 0)
        CAAML.addNode(dom, spmd, feature.info.uncertainty, 'caaml:uncertaintyOfMeas', 'm2kg-1');

      if (feature.info.thickness || feature.info.thickness === 0)
        CAAML.addNode(dom, spmd, feature.info.thickness, 'caaml:probedThickness', 'cm');

      if (feature.info.pointprofile) {
        CAAML.addPointProfile(dom, profile, sp, feature,
          [['uomDepth', 'cm'], ['uomSpecSurfArea', 'm2kg-1']],
          ['caaml:depth', 'caaml:specSurfArea']
        );

        return;
      }

      for (i = feature.layers.length - 1; i >= 0; --i) {
        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', 'cm');
        CAAML.addNode(dom, ob, round(l.top - l.bottom, 4), 'caaml:thickness', 'cm');
        CAAML.addNode(dom, ob, round(l.value, 4), 'caaml:specSurfArea', 'm2kg-1');
      }
    });
  };

  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) {
    profile.ct.elements.forEach(function (test) {
      var ct = node.appendChild(dom.createElement('caaml:ComprTest'));

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

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

        var fo = ct.appendChild(dom.createElement('caaml:failedOn'));

        var cl = fo.appendChild(dom.createElement('caaml:Layer'));
        if (l.comment) {
          var md = cl.appendChild((dom.createElement('caaml:metaData')));
          CAAML.addNode(dom, md, l.comment, 'caaml:comment');
        }
        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) {
    profile.ect.elements.forEach(function (test) {
      var ect = node.appendChild(dom.createElement('caaml:ExtColumnTest'));

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

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

        var fo = ect.appendChild(dom.createElement('caaml:failedOn'));

        var el = fo.appendChild(dom.createElement('caaml:Layer'));
        if (l.comment) {
          var md = el.appendChild(dom.createElement('caaml:metaData'));
          CAAML.addNode(dom, md, l.comment, 'caaml:comment');
        }
        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) {
    profile.rb.elements.forEach(function (test) {
      var rb = node.appendChild(dom.createElement('caaml:RBlockTest'));

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

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

        var fo = rb.appendChild(dom.createElement('caaml:failedOn'));

        var cl = fo.appendChild(dom.createElement('caaml:Layer'));
        if (l.comment) {
          var md = cl.appendChild(dom.createElement('caaml:metaData'));
          CAAML.addNode(dom, md, l.comment, 'caaml:comment');
        }
        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) {
    profile.sf.elements.forEach(function (test) {
      var sf = node.appendChild(dom.createElement('caaml:ShearFrameTest'));

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

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

        var fo = sf.appendChild(dom.createElement('caaml:failedOn'));

        var cl = fo.appendChild(dom.createElement('caaml:Layer'));
        if (l.comment) {
          var md = cl.appendChild(dom.createElement('caaml:metaData'));
          CAAML.addNode(dom, md, l.comment, 'caaml:comment');
        }
        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) {
    profile.saw.elements.forEach(function (test) {
      var saw = node.appendChild(dom.createElement('caaml:PropSawTest'));

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

        var fo = saw.appendChild(dom.createElement('caaml:failedOn'));

        var cl = fo.appendChild(dom.createElement('caaml:Layer'));
        if (l.comment) {
          var md = cl.appendChild(dom.createElement('caaml:metaData'));
          CAAML.addNode(dom, md, l.comment, 'caaml:comment');
        }
        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.addAppInfo = function (dom, node) {
    CAAML.addNode(dom, node, 'niViz', 'caaml:application');
    CAAML.addNode(dom, node, '1.0', 'caaml:applicationVersion');
  };

  /**
   * 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.gmlid || niviz.CAAML.defaults.default_profile_gmlid || 'NA';
    gmlid.replace(' ', '');

    CAAML.addNS(root, profile);
    CAAML.addMetaData(station, profile, dom, root);
    CAAML.addTimeRef(profile, dom, root);
    CAAML.addSrcRef(profile, dom, root);
    CAAML.addLocRef(station, dom, root);
    CAAML.addSnowProfile(station, profile, dom, root);
    CAAML.addAppInfo(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.CAAML = CAAML;

}(niviz));