// laserluxKmlTranslator.js
// 
// Versions
// 0.01 last version before 1/11/16
// 0.02 1/11/16 - fixed issue with the header printing out an undefined due to blank line
// 0.03 1/13/16 - added support for Edge browser
// 0.04 1/14/16 - made record reading code dynamic so that column header names are used to index instead of
//                array indices
// 0.05 1/14/16 - bug fix - still used array indices in a spot
// 1.0  2/15/2020 - allow for min valid, marginal0, marginal1, marginal2, good values and RGB values


String.prototype.padLeft = function(len,ch)
{
    if ( !ch )
        ch = ' ';
    var ans = this;
    while( ans.length < len )
        ans = ch + ans;
    return ans;
}

var gsNS = {name:"gsNS", 
    description:"namespace for Gamma-Scientific functions",
    parserVersion : '1.0',
    indent : '  ',  //string of spaces used to define atom of indentation
    delimiter : ',',
    lastLon : false,
    lastLat : false,
    tzo : '00',  // time zone offset string
    replaceFileLimits : false, // if false, limits in csv file will be used (validMin, marginal0Min, marignal1Min, marginal2Min goodMin)
	overlayLinkLocation : 'http://www.roadvista.com/images/RoadVistaLogo.png'
};

gsNS.NUM_MARGINAL_THRESHOLDS = 3;

//-------------------------------------------------------------------------
// element
gsNS.element = function(name)
{
    this.nodeName = name;
    this.nodeValue = '';
    this.attributes = [];
    this.childNodes = [];
};
gsNS.element.prototype.createElement = function(name)
{
    var el = new gsNS.element(name);
    return el;
};
gsNS.element.prototype.setAttribute = function(name, val)
{
    var attribute = {nodeName:name,nodeValue:val};
    this.attributes.push(attribute);
};
gsNS.element.prototype.appendChild = function(child)
{
    this.childNodes.push(child);
};
gsNS.element.prototype.createTextNode = function(text)
{
    var nd = this.createElement('#text');
    nd.nodeValue = text;
    return nd;
};

gsNS.element.prototype.getEscapedNodeValue = function()
{
    var value = this.nodeValue;
    if ( value == undefined )        
        return "";
    if ( -1 == value.indexOf("CDATA") )
        value = value.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\"/g,"&quot;").replace(/\'/g,"&apos;");
    return value;
}

gsNS.element.prototype.csvTranslate = function(csvData,params) {
	var allfound = true;
	console.log('Parsing Command Line Arguements...');
    if (typeof params !== 'undefined') {
        if( typeof params.validMin != 'undefined' ) {
            gsNS.validMin.threshold = params.validMin;
		}
		else {
			allfound = false;
		}
        if( typeof params.validR != 'undefined' ) {
            gsNS.validMin.color.red = params.validR;
		}
		else {
			allfound = false;
		}		
        if( typeof params.validG != 'undefined' ) {
            gsNS.validMin.color.green = params.validG;
		}
		else {
			allfound = false;
		}			
        if( typeof params.validB != 'undefined' ) {
            gsNS.validMin.color.blue = params.validB;
		}
		else {
			allfound = false;
		}		

        if( typeof params.marginal_0Min != 'undefined' ) {
            gsNS.marginalMin[0].threshold = params.marginal_0Min;
		}
		else {
			allfound = false;
		}
        if( typeof params.marginal_0R != 'undefined' ) {
            gsNS.marginalMin[0].color.red = params.marginal_0R;
		}
		else {
			allfound = false;
		}		
        if( typeof params.marginal_0G != 'undefined' ) {
            gsNS.marginalMin[0].color.green = params.marginal_0G;
		}
		else {
			allfound = false;
		}			
        if( typeof params.marginal_0B != 'undefined' ) {
            gsNS.marginalMin[0].color.blue = params.marginal_0B;
		}
		else {
			allfound = false;
		}	
		
		if( typeof params.marginal_1Min != 'undefined' ) {
            gsNS.marginalMin[1].threshold = params.marginal_1Min;
		}
		else {
			allfound = false;
		}
        if( typeof params.marginal_1R != 'undefined' ) {
            gsNS.marginalMin[1].color.red = params.marginal_1R;
		}
		else {
			allfound = false;
		}		
        if( typeof params.marginal_1G != 'undefined' ) {
            gsNS.marginalMin[1].color.green = params.marginal_1G;
		}
		else {
			allfound = false;
		}			
        if( typeof params.marginal_1B != 'undefined' ) {
            gsNS.marginalMin[1].color.blue = params.marginal_1B;
		}
		else {
			allfound = false;
		}	
		
		if( typeof params.marginal_2Min != 'undefined' ) {
            gsNS.marginalMin[2].threshold = params.marginal_2Min;
		}
		else {
			allfound = false;
		}
        if( typeof params.marginal_2R != 'undefined' ) {
            gsNS.marginalMin[2].color.red = params.marginal_2R;
		}
		else {
			allfound = false;
		}		
        if( typeof params.marginal_2G != 'undefined' ) {
            gsNS.marginalMin[2].color.green = params.marginal_2G;
		}
		else {
			allfound = false;
		}			
        if( typeof params.marginal_2B != 'undefined' ) {
            gsNS.marginalMin[2].color.blue = params.marginal_2B;
		}
		else {
			allfound = false;
		}	
		
        if( typeof params.goodMin != 'undefined' ) {
            gsNS.goodMin.threshold = params.goodMin;
		}
		else {
			allfound = false;
		}
        if( typeof params.goodR != 'undefined' ) {
            gsNS.goodMin.color.red = params.goodR;
		}
		else {
			allfound = false;
		}		
        if( typeof params.goodG != 'undefined' ) {
            gsNS.goodMin.color.green = params.goodG;
		}
		else {
			allfound = false;
		}			
        if( typeof params.goodB != 'undefined' ) {
            gsNS.goodMin.color.blue = params.goodB;
		}
		else {
			allfound = false;
		}
		if( typeof params.overlayLink != 'undefined' ) {			
			gsNS.overlayLinkLocation = params.overlayLink
			console.log('Using ' + gsNS.overlayLinkLocation + ' for logo');
		}
		else {
			allfound = false;
		}		
		if (allfound == true) {
			gsNS.replaceFileLimits = true;
			console.log('Will Replace File Limits and Colors From Command Line Data');
		}
		else {		
			console.log('Using CSV File Limits and Colors');
		}
    }
	
	
    if (typeof exports == 'undefined' && gsNS.replaceFileLimits ) 
    {
        gsNS.goodMin.threshold = parseFloat(document.getElementById('goodInput').value);
        var marginalInput = parseFloat(document.getElementById('marginalInput').value);
        for(var ndx = 0; ndx < gsNS.marginalMin.length; ndx++)
        {
            gsNS.marginalMin[ndx].threshold = marginalInput;
        }        
        gsNS.validMin.threshold = parseFloat(document.getElementById('validInput').value);
    }
	
	
    csvData = csvData.replace(/\r/g,"");
    var lines = csvData.split("\n");        
    var kml = this.createElement('kml');
    kml.setAttribute('xmlns', 'http://www.opengis.net/kml/2.2');
    kml.setAttribute('xmlns:gx', 'http://www.google.com/kml/ext/2.2');
    this.appendChild(kml);
    var doctag = this.createElement("Document");
    kml.appendChild(doctag);
    var desc = this.createElement('description');
    
    gsNS.delimiter = -1 < lines[0].indexOf(";") ? ';' : ',';
    var tv = lines[1].split(gsNS.delimiter);    
    desc.appendChild(this.createTextNode(tv[0]+'='+tv[1]));
    desc.appendChild(this.createTextNode('laserlux KML Parser Version=' + gsNS.parserVersion));
    var i = 2;
    while (lines[i].indexOf('Record #') == -1)
    {
        tv = lines[i].split(gsNS.delimiter);
        if (lines[i].indexOf('Timezone Offset') !== -1)
        {
            gsNS.tzo = tv[1];            
        }
        if (lines[i].indexOf('Minimum Good Value') !== -1)
        {
            gsNS.goodMin.parseLine(lines[i]);
        }
        if (lines[i].indexOf('Minimum Marginal Value') !== -1)
        {
            if ( lines[i]['Minimum Marginal Value'.length] == '-' )
            {
                var ndx = parseInt(lines[i]['Minimum Marginal Value'.length + 1]);
                if ( 0 <= ndx && ndx < gsNS.marginalMin.length )
                {
                    gsNS.marginalMin[ndx].parseLine(lines[i]);
                }
            }
            else
            {
                for(var ndx = 0; ndx < gsNS.marginalMin.length; ndx++ )
                {
                    gsNS.marginalMin[ndx].parseLine(lines[i]);
                }
            }
        }
        if (lines[i].indexOf('Minimum Valid Value') !== -1)
        {
            gsNS.validMin.parseLine(lines[i]);
        }        
        if (typeof tv[1] !== 'undefined')
        {
            desc.appendChild(this.createTextNode(tv[0]+'='+tv[1]));
        }
        i++;
    }
    desc.appendChild(this.createTextNode('Data Limits:'));
    desc.appendChild(this.createTextNode(gsNS.indent + gsNS.goodMin.label + ' = ' + gsNS.goodMin.threshold.toFixed(1)));
    for(var ndx = 0; ndx < gsNS.marginalMin.length; ndx++ )
    {
        desc.appendChild(this.createTextNode(gsNS.indent + gsNS.marginalMin[ndx].label + ' = ' + gsNS.marginalMin[ndx].threshold.toFixed(1)));
    }
    desc.appendChild(this.createTextNode(gsNS.indent + gsNS.validMin.label + ' = ' + gsNS.validMin.threshold.toFixed(1)));
    
    doctag.appendChild(desc);
    
    this.createScreenOverlay(doctag);
    this.addLegend(doctag);
    this.addStyles(doctag);
    this.addRetroInfo(doctag,lines);
    
    var kmlS = kml.printNode();
    return '<?xml version="1.0" encoding="UTF-8"?>\n'+kmlS;
}

gsNS.element.prototype.createScreenOverlay = function(node)
{
    var sOverlay = this.createElement('ScreenOverlay');    
    var name = this.createElement('name');
    sOverlay.appendChild(name);
    name.appendChild(this.createTextNode('OverlayLogo'));
    var icon = this.createElement('Icon');
    var href = this.createElement('href');
    sOverlay.appendChild(icon);
    icon.appendChild(href);
	href.appendChild(this.createTextNode(gsNS.overlayLinkLocation));
    var oXY = this.createElement('overlayXY');
    oXY.setAttribute('x','0.5');
    oXY.setAttribute('y','1');
    oXY.setAttribute('xunits','fraction');
    oXY.setAttribute('yunits','fraction');
    sOverlay.appendChild(oXY);
    var sXY = this.createElement('screenXY');
    sXY.setAttribute('x','0.5');
    sXY.setAttribute('y','0.99');
    sXY.setAttribute('xunits','fraction');
    sXY.setAttribute('yunits','fraction');
    sOverlay.appendChild(sXY);
    var rXY = this.createElement('rotationXY');
    rXY.setAttribute('x','0.5');
    rXY.setAttribute('y','0.5');
    rXY.setAttribute('xunits','fraction');
    rXY.setAttribute('yunits','fraction');
    sOverlay.appendChild(rXY);
    var sz = this.createElement('size');
    sz.setAttribute('x','0.1');
    sz.setAttribute('y','0');
    sz.setAttribute('xunits','fraction');
    sz.setAttribute('yunits','fraction');
    sOverlay.appendChild(sz);
    node.appendChild(sOverlay);
}

gsNS.element.prototype.addLegend = function(node)
{
    var fold = this.createElement('Folder');    
    var name = this.createElement('name');
    name.appendChild(this.createTextNode('Legend: RL mcd/m2/lux'));
    fold.appendChild(name);
    this.addLegendPlacemark(fold, gsNS.goodMin.color.getHtmlColor(), 'Above ' + gsNS.goodMin.threshold.toFixed(1)); 
    var value = gsNS.goodMin.threshold;
    for(var ndx = gsNS.marginalMin.length - 1; 0 <= ndx; ndx--)
    {
        var obj = gsNS.marginalMin[ndx];
        this.addLegendPlacemark(fold, obj.color.getHtmlColor(), 'Between ' + obj.threshold.toFixed(1) + ' and ' + value.toFixed(1));
        value = obj.threshold;
    }
    this.addLegendPlacemark(fold, gsNS.validMin.color.getHtmlColor(), 'Below ' + value.toFixed(1));
    this.addLegendPlacemark(fold,'000000','No Data');
    var op = this.createElement('open');
    op.appendChild(this.createTextNode('1'));
    fold.appendChild(op);
    var vis = this.createElement('visibility');
    vis.appendChild(this.createTextNode('1'));
    fold.appendChild(vis);
    node.appendChild(fold);
}

gsNS.element.prototype.addLegendPlacemark = function(node,color,legend)
{
    var str = '<![CDATA[<span style="color:#' + color + ';"><b>' + legend + '</b></span>]]>';
    var pm = this.createElement('Placemark');
    var name = this.createElement('name');
    name.appendChild(this.createTextNode(str));
    var surl = this.createElement('styleUrl');
    surl.appendChild(this.createTextNode('#gv_legend'));
    var vis = this.createElement('visibility');
    vis.appendChild(this.createTextNode('1'));
    pm.appendChild(name);
    pm.appendChild(surl);
    pm.appendChild(vis);
    node.appendChild(pm);
}

gsNS.element.prototype.addStyles = function(node)
{
    this.addStyle(node,'Style-Odometer','http://maps.google.com/mapfiles/kml/shapes/arrow.png',false,'0.5');
    var colors = ['ffff0000','ffffa000','ffa0ff00','ff00ff00','ff00ffa0','ff00ffff','ff00a0ff','ff0000ff','ffa000ff','ffff00a0'];
    for (i = 0; i < 10; i++)
    {
        this.addStyleGroup(node,i.toString(),colors[i]);
    }
}
gsNS.element.prototype.addStyleGroup = function(node,num,color)
{
    var id = 'Style-Momentary'+num;
    this.addStyle(node,id,'http://maps.google.com/mapfiles/kml/paddle/wht-blank.png',false,false,color);
    id = 'Style-Start'+num;
    this.addStyle(node,id,'http://maps.google.com/mapfiles/kml/paddle/wht-circle.png',false,false,color);
    id = 'Style-End'+num;
    this.addStyle(node,id,'http://maps.google.com/mapfiles/kml/paddle/wht-square.png','0.6',false,color);
}

gsNS.element.prototype.addStyle = function(node,id,href,cScale,lScale,color)
{
    var style = this.createElement('Style');
    style.setAttribute('id',id);
    if (lScale)
    {
        var labelStyle = this.createElement('LabelStyle');
        var labelScal = this.createElement('scale');
        labelScal.appendChild(this.createTextNode(lScale));
        labelStyle.appendChild(labelScal);
        style.appendChild(labelStyle);
    }
    var lineStyle = this.createElement('LineStyle');
    var wid = this.createElement('width');
    wid.appendChild(this.createTextNode('2'));
    lineStyle.appendChild(wid);
    style.appendChild(lineStyle);
    
    var iconStyle = this.createElement('IconStyle');
    if (lScale)
    {
        var sc = this.createElement('scale');
        sc.appendChild(this.createTextNode(lScale));
        iconStyle.appendChild(sc);
    }
    var icon = this.createElement('Icon');
    var hrefE = this.createElement('href');
    hrefE.appendChild(this.createTextNode(href));
    icon.appendChild(hrefE);
    iconStyle.appendChild(icon);
    
    if (color)
    {
        var colorE = this.createElement('color');
        colorE.appendChild(this.createTextNode(color));
        iconStyle.appendChild(colorE);
    }
    if (cScale)
    {
        var cScaleE = this.createElement('scale');
        cScaleE.appendChild(this.createTextNode(cScale));
        iconStyle.appendChild(cScaleE);
    }
    style.appendChild(iconStyle);
    node.appendChild(style);
}

gsNS.element.prototype.addRetroInfo = function(node,lines)
{
    var fold = this.createElement('Folder');
    var name = this.createElement('name');
    name.appendChild(this.createTextNode('Retroreflectivity Data, Default_Route'));
    fold.appendChild(name);
        
    var conds = [];
    var momentary = [];
    
    var recStart = 0;
    for (var i = 0; i < lines.length; i++)
    {
        var tmp = lines[i].split(gsNS.delimiter);
        if (tmp[0].substring(0,14) == 'Road Condition')
        {
            conds.push(tmp[1]);
            if (tmp[2].substring(0,9) == 'momentary')
            {
                momentary.push(true);
            }
            else
            {
                momentary.push(false);
            }
        }
        if (tmp[0] == 'Record #')
        {
            recStart = i;
            break;
        }
    }
    var records = gsNS.pullRecords(lines,recStart);
    var vcol = lines[recStart].split(gsNS.delimiter);
    gsNS.lastLon = false;
    gsNS.lastLat = false;
    
    // any right data?
    var anyRight = false;
    for (var i = 0; i < records.length; i++)
    {
        if (records[i]['Valid_Scans_Right'] !== undefined && records[i]['Valid_Scans_Right'].length > 0)
        {
            anyRight = true;
            break;
        }
    }
        
    for (var i = 0; i < records.length; i++) 
    {
        fold.storeRecord(records[i], vcol);
    }

    if (anyRight)
    {
        gsNS.lastLon = gsNS.lastLat = false;
        var foldR = this.createElement('Folder');
        var nameR = this.createElement('name');
        nameR.appendChild(this.createTextNode('Retroreflectivity Data, Default_Route (R)'));
        foldR.appendChild(nameR);
        for (var i = 0; i < records.length; i++) 
        {
            foldR.storeRecord(records[i], vcol, true);
        }
        node.appendChild(foldR);
    }
    node.appendChild(fold);    
    this.addRoadConditions(node,conds,records,momentary);
    this.addOdometerMarkers(node,records);
}

gsNS.element.prototype.addOdometerMarkers = function(node,records)
{
    var fold = this.createElement('Folder');
    var name = this.createElement('name');
    name.appendChild(this.createTextNode('Odometer Markers'));
    fold.appendChild(name);
    for (var i = 0; i < records.length; i++)
    {
        if (records[i]['Record #'].indexOf('(Road Condition)') == -1)
            this.genOdo(fold,records[i]);
    }
    node.appendChild(fold);
}

gsNS.element.prototype.addRoadConditions = function(node,conds,records,momentarys)
{
    var fold = this.createElement('Folder');
    var name = this.createElement('name');
    name.appendChild(this.createTextNode('Road Conditions'));
    fold.appendChild(name);
    
    var colors = ['0000ff','00a0ff','00ffa0','00ff00','a0ff00','ffff00','ffa000','ff0000','ff00a0','a000ff'];
    
    for (var i = 0; i < conds.length; i++)
    {
        this.addRoadCondition(fold,conds[i],colors[i],records,momentarys[i],i);
    }
    
    node.appendChild(fold);
}

gsNS.element.prototype.addRoadCondition = function(node,cond,color,records,momentary,i)
{
    var fold = this.createElement('Folder');
    var name = this.createElement('name');
    name.appendChild(this.createTextNode('<![CDATA[<span style="color:#'+color+';">'+cond+'</span>]]>'));
    fold.appendChild(name);
    var fold2 = this.createElement('Folder');
    var nm = this.createElement('name');
    nm.appendChild(this.createTextNode('Markers'));
    fold2.appendChild(nm);
    for (var j = 0; j < records.length; j++)
    {
        if (records[j]['Road_Conditions'].indexOf(i.toString()) !== -1)
        {
            var k = j+1;
            for (; k < records.length; k++)
            {
                if (records[k]['Road_Conditions'].indexOf(i.toString()) == -1)
                {
                    break;
                }
            }
            var style = momentary ? 'Momentary' : 'Start';
            this.genPlacemark(fold2,records[j],cond,style,i);
            if (k > j+1)
            {
                if (k != records.length)
                {
                    this.genPlacemark(fold2,records[k],cond,'End',i);
                }
                j = k;
            }
        }
    }
    fold.appendChild(fold2);
    
    var colorsL = ['ffff0000','ffffa000','ffa0ff00','ff00ff00','ff00ffa0','ff00ffff','ff00a0ff','ff0000ff','ffa000ff','ffff00a0'];
    var fold3 = this.createElement('Folder');
    var nm3 = this.createElement('name');
    nm3.appendChild(this.createTextNode('Lines'));
    fold3.appendChild(nm3);
    for (j = 0; j < records.length; j++)
    {
        if (records[j]['Road_Conditions'].indexOf(i.toString()) !== -1)
        {
            if (j+1 < records.length && records[j+1]['Road_Conditions'].indexOf(i.toString()) !== -1)
            {
                var rec1 = records[j];
                var k = j; // followed original kml file to include j, doesn't seem to make sense for a line though
                for (; k < records.length; k++)
                {
                    var rec2 = records[k];
                    this.genLine(fold3,rec1,rec2,cond,colorsL[i]);
                    if (records[k]['Road_Conditions'].indexOf(i.toString()) == -1)
                    {
                        break;
                    }
                    rec1 = rec2;
                }
                j = k;
            }
        }
    }
    fold.appendChild(fold3);
    node.appendChild(fold);
}

gsNS.element.prototype.genOdo = function( node, rec)
{
    var pm = this.createElement('Placemark');
    var ts = this.createElement('TimeStamp');
    var when = this.createElement('when');
    when.appendChild(this.createTextNode(rec['Date'] + 'T' + rec['Time'] + gsNS.tzo + ':00'));
    ts.appendChild(when);
    pm.appendChild(ts);
    var name = this.createElement('name');
    name.appendChild(this.createTextNode(rec['Odometer']));
    pm.appendChild(name);
    var vis = this.createElement('visibility');
    vis.appendChild(this.createTextNode('0'));
    pm.appendChild(vis);
    var desc = this.createElement('description');
    desc.appendChild(this.createTextNode('Odometer='+rec['Odometer']));
    pm.appendChild(desc);
    var surl = this.createElement('styleUrl');
    surl.appendChild(this.createTextNode('#Style-Odometer'));
    pm.appendChild(surl);
    var pt = this.createElement('Point');
    var alt = this.createElement('altitudeMode');
    alt.appendChild(this.createTextNode('clampToGround'));
    pt.appendChild(alt);
    var coords = this.createElement('coordinates');
    coords.appendChild(this.createTextNode(rec['Longitude']+','+rec['Latitude']+',0.0'));
    pt.appendChild(coords);
    pm.appendChild(pt);
    node.appendChild(pm);
}

gsNS.element.prototype.genLine = function(node,rec1,rec2,cond,color)
{
    var pm = this.createElement('Placemark');
    var nm = this.createElement('name');
    nm.appendChild(this.createTextNode(cond));
    pm.appendChild(nm);
    var ts = this.createElement('TimeStamp');
    var when = this.createElement('when');
    when.appendChild(this.createTextNode(rec2['Date'] + 'T' + rec2['Time'] + gsNS.tzo + ':00'));
    ts.appendChild(when);
    pm.appendChild(ts);
    var vis = this.createElement('visibility');
    vis.appendChild(this.createTextNode('1')); //TBD
    pm.appendChild(vis);
    var ls = this.createElement('LineString');
    var alt = this.createElement('altitudeMode');
    alt.appendChild(this.createTextNode('clampToGround'));
    ls.appendChild(alt);
    var coords = this.createElement('coordinates');
    coords.appendChild(this.createTextNode(rec1['Longitude'] + ',' + rec1['Latitude'] + ',0.0 ' + rec2['Longitude'] + ',' + rec2['Latitude'] + ',0.0'));
    ls.appendChild(coords);
    var tess = this.createElement('tessellate');
    tess.appendChild(this.createTextNode('1'));
    ls.appendChild(tess);
    pm.appendChild(ls);
    var sty = this.createElement('Style');
    var lsty = this.createElement('LineStyle');
    var col = this.createElement('color');
    col.appendChild(this.createTextNode(color));
    lsty.appendChild(col);
    var wid = this.createElement('width');
    wid.appendChild(this.createTextNode('2'));
    lsty.appendChild(wid);
    sty.appendChild(lsty);
    pm.appendChild(sty);
    node.appendChild(pm);
}

gsNS.element.prototype.genPlacemark = function(node,rec,cond,style,condNum)
{
    var pm = this.createElement('Placemark');
    var name = this.createElement('name');
    if (style !== 'End')
    {
        name.appendChild(this.createTextNode(cond));
    }
    pm.appendChild(name);
    var ts = this.createElement('TimeStamp');
    var when = this.createElement('when');
    when.appendChild(this.createTextNode(rec['Date'] + 'T' + rec['Time'] + gsNS.tzo + ':00'));
    ts.appendChild(when);
    pm.appendChild(ts);
    var desc = this.createElement('description');
    desc.appendChild(this.createTextNode(cond+'('+style+')'));
    desc.appendChild(this.createTextNode('Odometer='+rec['Odometer']));
    desc.appendChild(this.createTextNode('Date='+rec['Date']));
    desc.appendChild(this.createTextNode('Time='+rec['Time']));
    desc.appendChild(this.createTextNode('Latitude='+rec['Latitude']));
    desc.appendChild(this.createTextNode('Longitude='+rec['Longitude']));
    pm.appendChild(desc);
    
    var sturl = this.createElement('styleUrl');
    sturl.appendChild(this.createTextNode('#Style-'+style+condNum.toString()));
    pm.appendChild(sturl);
    var point = this.createElement('Point');
    var ext = this.createElement('extrude');
    ext.appendChild(this.createTextNode('1'));
    point.appendChild(ext);
    var alt = this.createElement('altitudeMode');
    alt.appendChild(this.createTextNode('clampToGround'));
    point.appendChild(alt);
    var coords = this.createElement('coordinates');
    coords.appendChild(this.createTextNode(rec['Longitude']+','+rec['Latitude']+',0.0'));
    point.appendChild(coords);
    pm.appendChild(point);
    node.appendChild(pm);
}

gsNS.element.prototype.storeRecord = function(rec,vCol,RFlag)
{
    if (rec['Record #'].substring(0,15) == '(Road Condition')
    {
        return;
    } 
    if (rec['Record #'].substring(0,10) == '(Data Flag')
    {
        return;
    } 
    var pm = this.createElement('Placemark');
    var ts = this.createElement('TimeStamp');
    var when = this.createElement('when');
    when.appendChild(this.createTextNode(rec['Date'] + 'T' + rec['Time'] + gsNS.tzo + ':00'));
    ts.appendChild(when);
    pm.appendChild(ts);
    var name = this.createElement('name');
    name.appendChild(this.createTextNode(rec['Record #']));
    pm.appendChild(name);
    var desc = this.createElement('description');
    for (var i = 0; i < vCol.length; i++)
    {
        desc.appendChild(this.createTextNode(vCol[i] + ' = ' + rec[vCol[i]]));
    }
    pm.appendChild(desc);
    if (gsNS.lastLon)
    {
        var LS = this.createElement('LineString');
        var alt = this.createElement('altitudeMode');
        alt.appendChild(this.createTextNode('clampToGround'));
        LS.appendChild(alt);
        var coords = this.createElement('coordinates');
        coords.appendChild(this.createTextNode(gsNS.lastLon + ',' + gsNS.lastLat + ',0.0 ' + rec['Longitude'] + ',' + rec['Latitude'] + ',0.0'));
        LS.appendChild(coords);
        var tess = this.createElement('tessellate');
        tess.appendChild(this.createTextNode('1'));
        LS.appendChild(tess);
        pm.appendChild(LS);
        var sty = this.createElement('Style');
        var Lsty = this.createElement('LineStyle');
        var color = '4f000000'; // default not data
        var ra = RFlag ? rec['Retro_Right_Average']:rec['Retro_Left_Average'];
        if ( gsNS.validMin.threshold <= ra )
        {
            color = '4f' + gsNS.validMin.color.getKmlColor();
            if ( gsNS.goodMin.threshold <= ra )
            {                
                color = '4f' + gsNS.goodMin.color.getKmlColor();
            }
            else
            {
                for(var ndx = 0; ndx < gsNS.marginalMin.length; ndx++)
                {
                    if ( gsNS.marginalMin[ndx].threshold <= ra )
                    {                
                        color = '4f' + gsNS.marginalMin[ndx].color.getKmlColor();
                    }
                }
            }
        }
        var col = this.createElement('color');
        col.appendChild(this.createTextNode(color));
        Lsty.appendChild(col);
        var wid = this.createElement('width');
        wid.appendChild(this.createTextNode('8'));
        Lsty.appendChild(wid);
        sty.appendChild(Lsty);
        pm.appendChild(sty);        
    }
    this.appendChild(pm);
    
    gsNS.lastLon = rec['Longitude'];
    gsNS.lastLat = rec['Latitude'];
}

gsNS.element.prototype.printNode = function(pad)
{
    if (typeof pad == 'undefined')
    {
        pad = gsNS.indent;
    }
    //if ( this.nodeName == "Placemark" )
        //console.log(this);    
    var str = pad + '<' + this.nodeName;
    var attr = this.attributes;
    if (typeof attr !== 'undefined')
    {
        for (var i = 0; i < attr.length; i++)
        {
            str += ' ' + attr[i].nodeName + '="' + attr[i].nodeValue + '"';
        }
    }
    var kids = this.childNodes;
    if (kids.length == 0)
    {
        str += '/>\n';
    }
    else
    {
        if (kids.length == 1 && kids[0].nodeName == '#text')
        {
            str += '>' + kids[0].getEscapedNodeValue() + '</' + this.nodeName + '>\n';
        }
        else
        {
            str += '>\n';
            for (var i = 0; i < kids.length; i++)
            {
                if (kids[i].nodeName.substring(0,1) !== '#')
                {
                    str += kids[i].printNode(pad + gsNS.indent);
                }
            }
            // now print out the text nodes
            var tPad = pad + gsNS.indent;
            for (var i = 0; i < kids.length; i++)
            {
                if (kids[i].nodeName == '#text')
                {
                    str += tPad + kids[i].getEscapedNodeValue() +'\n';
                }
            }        
            str += pad + '</' + this.nodeName + '>\n';
        }
    }
    return str;
}

//-------------------------------------------------------------------------
// ThresholdClass
gsNS.ThresholdClass = function(threshold,label,color)
{
    this.threshold = threshold;
    this.label = label;
    if ( color )
    {
        this.color = color;
    }
    else
    {
        this.color = new gsNS.ColorClass(255,255,0);
    }
}
gsNS.ThresholdClass.prototype.constructor = gsNS.ThresholdClass;
gsNS.ThresholdClass.prototype.parseLine = function(line)
{
    if ( !line )
        return;
    if ( gsNS.replaceFileLimits )
        return;
    var tv = line.split(gsNS.delimiter);
    if ( tv.length < 2 )
        return;
    this.label = tv[0];
    this.threshold = parseFloat(tv[1]);
    if ( 4 < tv.length )
    {
        this.color.red = parseInt(tv[2]);
        this.color.green = parseInt(tv[3]);
        this.color.blue = parseInt(tv[4]);
    }
}

// ColorClass
gsNS.ColorClass = function(red,green,blue)
{
    this.red = red;
    this.green = green;
    this.blue = blue;
}
gsNS.ColorClass.prototype.constructor = gsNS.ColorClass;
gsNS.ColorClass.prototype.getHtmlColor = function()
{
    var html = "";
    html += this.red.toString(16).padLeft(2,"0");
    html += this.green.toString(16).padLeft(2,"0");
    html += this.blue.toString(16).padLeft(2,"0");
    return html;
};

gsNS.ColorClass.prototype.getKmlColor = function()
{
    var html = "";
    html += this.blue.toString(16).padLeft(2,"0");
    html += this.green.toString(16).padLeft(2,"0");
    html += this.red.toString(16).padLeft(2,"0");
    return html;
};

// 25.0, red,    default value for minimum average retro reflectance to be considered valid
// 50.0, yellow, default value for minimum average retro reflectance to be considered marginal
// 100.0, green, default value fof minimum average retro reflectance to be considered good
gsNS.validMin = new gsNS.ThresholdClass(25.0, 'Minimum Valid Value', new gsNS.ColorClass(255,0,0));  
gsNS.marginalMin = []; 
for(var ndx = 0; ndx < gsNS.NUM_MARGINAL_THRESHOLDS; ndx++ )
{
    gsNS.marginalMin[gsNS.marginalMin.length] = new gsNS.ThresholdClass(50.0, "Minimum Marginal Value-" + ndx.toString(), new gsNS.ColorClass(255,255,0));
}
gsNS.goodMin = new gsNS.ThresholdClass(100.0, "Minimum Good Value", new gsNS.ColorClass(0,255,0));      

//-------------------------------------------------------------------------
// namespace functions
gsNS.pullRecords = function(lines,start)
{
    var head = lines[start].split(gsNS.delimiter);
    var records = [];
    for (var i = start+1; i < lines.length; i++)
    {
        var v = lines[i].split(gsNS.delimiter);
        if (v[0].length > 0)
        {
            if (v.length !== head.length)
            {
                console.log('Line ' + i +' has a different number of columns than the header'); // was alert()
                return;
            }
            records[records.length] = [];            
            for (var j = 0; j < head.length; j++)
            {
                records[records.length-1][head[j]] = v[j];
            }
        }
    }
    return records;    
}

//-------------------------------------------------------------------------
// external (HTML/js) interface
gsNS.openFile = function(evt)
{
    var fileInput = document.getElementById('fileInput');        
    gsNS.replaceFileLimits = document.getElementById('replaceMins').checked;        
    var files = fileInput.files;
    for (var j = 0; j < files.length; j++)
    {
        this.readFile(files[j]);
    }
}

gsNS.readFile = function(file)
{
    var reader = new FileReader();
    reader.onload = function (e)
        {
            // this := FileReader namespace
            var csvData = reader.result;
            var doc = new gsNS.element('kml');
            var xml = doc.csvTranslate(csvData);
            output = file.name.substring(0,file.name.lastIndexOf('.'))+'.kml';
            gsNS.download(output,xml);
        }
    reader.readAsText(file);
}

gsNS.download = function(filename, text)
{
    var isIE = (window.ActiveXObject || "ActiveXObject" in window);
    var isEdge = navigator.userAgent.indexOf('Edge') !== -1;
    if (isIE || isEdge)
    {
        navigator.msSaveBlob(new Blob([text], {type: 'text/csv'}),filename);
    }
    else
    {
        var a = window.document.createElement('a');
        a.href = window.URL.createObjectURL(new Blob([text], {type: 'text/csv'}));        
        a.download = filename; // download not implemented in IE
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
}


//-------------------------------------------------------------------------
// cmd line
if (typeof exports !== 'undefined') {
	console.log('Laserlux KML Translator Version ' + gsNS.parserVersion);
    exports.conv = function(csv,params) {
		var doc = new gsNS.element('kml');
        return doc.csvTranslate(csv,params);
    };
}