/*
 * GPXParser.java
 * 
 * Copyright (c) 2012, AlternativeVision. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */

package org.alternativevision.gpx;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.alternativevision.gpx.beans.GPX;
import org.alternativevision.gpx.beans.Route;
import org.alternativevision.gpx.beans.Track;
import org.alternativevision.gpx.beans.Waypoint;
import org.alternativevision.gpx.extensions.IExtensionParser;
import org.alternativevision.gpx.types.FixType;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * <p>This class defines methods for parsing and writing gpx files.</p>
 * <br>
 * Usage for parsing a gpx file into a {@link GPX} object:<br>
 * <code>
 * GPXParser p = new GPXParser();<br>
 * FileInputStream in = new FileInputStream("inFile.gpx");<br>
 * GPX gpx = p.parseGPX(in);<br>
 * </code>
 * <br>
 * Usage for writing a {@link GPX} object to a file:<br>
 * <code>
 * GPXParser p = new GPXParser();<br>
 * FileOutputStream out = new FileOutputStream("outFile.gpx");<br>
 * p.writeGPX(gpx, out);<br>
 * out.close();<br>
 * </code>
 */
public class GPXParser {
	
	private ArrayList<IExtensionParser> extensionParsers = new ArrayList<IExtensionParser>();
	private Logger logger = Logger.getLogger(this.getClass().getName());
	private DocumentBuilderFactory docFactory= DocumentBuilderFactory.newInstance();
	private TransformerFactory tFactory = TransformerFactory.newInstance();
	private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ss");
	private SimpleDateFormat sdfZ = new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ss'Z'");
	/**
	 * Adds a new extension parser to be used when parsing a gpx steam
	 * 
	 * @param parser an instance of a {@link IExtensionParser} implementation
	 */
	public void addExtensionParser(IExtensionParser parser) {
		extensionParsers.add(parser);
	}
	
	
	/**
	 * Removes an extension parser previously added
	 * 
	 * @param parser an instance of a {@link IExtensionParser} implementation
	 */
	public void removeExtensionParser(IExtensionParser parser) {
		extensionParsers.remove(parser);
	}
	
	
	
	public GPX parseGPX(File gpxFile) throws IOException, ParserConfigurationException, SAXException, IOException {
		InputStream in = FileUtils.openInputStream(gpxFile);
		GPX gpx = parseGPX(in);
		in.close();
		return gpx;
	}
	
	/**
	 * Parses a stream containing GPX data
	 *   
	 * @param in the input stream
	 * @return {@link GPX} object containing parsed data, or null if no gpx data was found in the seream
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 */
	public GPX parseGPX(InputStream in) throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilder builder = docFactory.newDocumentBuilder();
		Document doc = builder.parse(in);
		Node firstChild = doc.getFirstChild();
		if( firstChild != null && GPXConstants.GPX_NODE.equals(firstChild.getNodeName())) {
			GPX gpx = new GPX();
			NamedNodeMap attrs = firstChild.getAttributes();
			for(int idx = 0; idx < attrs.getLength(); idx++) {
				Node attr = attrs.item(idx);
				if(GPXConstants.VERSION_ATTR.equals(attr.getNodeName())) {
					gpx.setVersion(attr.getNodeValue());
				} else if(GPXConstants.CREATOR_ATTR.equals(attr.getNodeName())) {
					gpx.setCreator(attr.getNodeValue());
				}
			}
			NodeList nodes = firstChild.getChildNodes();
			if(logger.isDebugEnabled())logger.debug("Found " +nodes.getLength()+ " child nodes. Start parsing ...");
			for(int idx = 0; idx < nodes.getLength(); idx++) {
				Node currentNode = nodes.item(idx);
				if(GPXConstants.WPT_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("Found waypoint node. Start parsing...");
					Waypoint w = parseWaypoint(currentNode);
					if(w!= null) {
						logger.info("Add waypoint to gpx data. [waypointName="+ w.getName() + "]");
						gpx.addWaypoint(w);
					}
				} else if(GPXConstants.TRK_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("Found track node. Start parsing...");
					Track trk = parseTrack(currentNode);
					if(trk!= null) {
						logger.info("Add track to gpx data. [trackName="+ trk.getName() + "]");
						gpx.addTrack(trk);
					}
				} else if(GPXConstants.EXTENSIONS_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("Found extensions node. Start parsing...");
					
					Iterator<IExtensionParser> it = extensionParsers.iterator();
					while(it.hasNext()) {
						IExtensionParser parser = it.next();
						Object data = parser.parseGPXExtension(currentNode);
						gpx.addExtensionData(parser.getId(), data);
					}
				} else if(GPXConstants.RTE_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("Found route node. Start parsing...");
					Route rte = parseRoute(currentNode);
					if(rte!= null) {
						logger.info("Add route to gpx data. [routeName="+ rte.getName() + "]");
						gpx.addRoute(rte);
					}
				}
			}
			//TODO: parse route node
			return gpx;
		} else {
			logger.error("FATAL!! - Root node is not gpx.");
		}
		return null;
	}
	
	/**
	 * Parses a wpt node into a Waypoint object 
	 * 
	 * @param node
	 * @return Waypoint object with info from the received node
	 */
	private Waypoint parseWaypoint(Node node) {
		if(node == null) {
			logger.error("null node received");
			return null;
		}
		Waypoint w = new Waypoint();
		NamedNodeMap attrs = node.getAttributes();
		//check for lat attribute
		Node latNode = attrs.getNamedItem(GPXConstants.LAT_ATTR);
		if(latNode != null) {
			Double latVal = null;
			try {
				latVal = Double.parseDouble(latNode.getNodeValue());
			} catch(NumberFormatException ex) {
				logger.error("bad lat value in waypoint data: " + latNode.getNodeValue());
			}
			w.setLatitude(latVal);
		} else {
			logger.warn("no lat value in waypoint data.");
		}
		//check for lon attribute
		Node lonNode = attrs.getNamedItem(GPXConstants.LON_ATTR);
		if(lonNode != null) {
			Double lonVal = null;
			try {
				lonVal = Double.parseDouble(lonNode.getNodeValue());
			} catch(NumberFormatException ex) {
				logger.error("bad lon value in waypoint data: " + lonNode.getNodeValue());
			}
			w.setLongitude(lonVal);
		} else {
			logger.warn("no lon value in waypoint data.");
		}
		
		NodeList childNodes = node.getChildNodes();
		if(childNodes != null) {
			for(int idx = 0; idx < childNodes.getLength(); idx++) {
				Node currentNode = childNodes.item(idx);
				if(GPXConstants.ELE_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found ele node in waypoint data");
					w.setElevation(getNodeValueAsDouble(currentNode));
				} else if(GPXConstants.TIME_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found time node in waypoint data");
					w.setTime(getNodeValueAsDate(currentNode));
				} else if(GPXConstants.NAME_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found name node in waypoint data");
					w.setName(getNodeValueAsString(currentNode));
				} else if(GPXConstants.CMT_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found cmt node in waypoint data");
					w.setComment(getNodeValueAsString(currentNode));
				} else if(GPXConstants.DESC_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found desc node in waypoint data");
					w.setDescription(getNodeValueAsString(currentNode));
				} else if(GPXConstants.SRC_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found src node in waypoint data");
					w.setSrc(getNodeValueAsString(currentNode));
				} else if(GPXConstants.MAGVAR_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found magvar node in waypoint data");
					w.setMagneticDeclination(getNodeValueAsDouble(currentNode));
				} else if(GPXConstants.GEOIDHEIGHT_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found geoidheight node in waypoint data");
					w.setGeoidHeight(getNodeValueAsDouble(currentNode));
				} else if(GPXConstants.LINK_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found link node in waypoint data");
					//TODO: parse link
					//w.setGeoidHeight(getNodeValueAsDouble(currentNode));
				} else if(GPXConstants.SYM_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found sym node in waypoint data");
					w.setSym(getNodeValueAsString(currentNode));
				} else if(GPXConstants.FIX_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found fix node in waypoint data");
					w.setFix(getNodeValueAsFixType(currentNode));
				} else if(GPXConstants.TYPE_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found type node in waypoint data");
					w.setType(getNodeValueAsString(currentNode));
				} else if(GPXConstants.SAT_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found sat node in waypoint data");
					w.setSat(getNodeValueAsInteger(currentNode));
				} else if(GPXConstants.HDOP_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found hdop node in waypoint data");
					w.setHdop(getNodeValueAsDouble(currentNode));
				} else if(GPXConstants.VDOP_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found vdop node in waypoint data");
					w.setVdop(getNodeValueAsDouble(currentNode));
				} else if(GPXConstants.PDOP_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found pdop node in waypoint data");
					w.setPdop(getNodeValueAsDouble(currentNode));
				} else if(GPXConstants.AGEOFGPSDATA_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found ageofgpsdata node in waypoint data");
					w.setAgeOfGPSData(getNodeValueAsDouble(currentNode));
				} else if(GPXConstants.DGPSID_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found dgpsid node in waypoint data");
					w.setDgpsid(getNodeValueAsInteger(currentNode));
				} else if(GPXConstants.EXTENSIONS_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("found extensions node in waypoint data");
					Iterator<IExtensionParser> it = extensionParsers.iterator();
					while(it.hasNext()) {
						IExtensionParser parser = it.next();
						Object data = parser.parseWaypointExtension(currentNode);
						w.addExtensionData(parser.getId(), data);
					}
				} 
			}
		} else {
			if(logger.isDebugEnabled())logger.debug("no child nodes found in waypoint");
		}
		
		return w;
	}
	
	private Track parseTrack(Node node) {
		if(node == null) {
			logger.error("null node received");
			return null;
		}
		Track trk = new Track();
		NodeList nodes = node.getChildNodes();
		if(nodes != null) {
			for(int idx = 0; idx < nodes.getLength(); idx++) {
				Node currentNode = nodes.item(idx);
				if(GPXConstants.NAME_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node name found");
					trk.setName(getNodeValueAsString(currentNode));
				} else if(GPXConstants.CMT_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node cmt found");
					trk.setComment(getNodeValueAsString(currentNode));
				} else if(GPXConstants.DESC_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node desc found");
					trk.setDescription(getNodeValueAsString(currentNode));
				} else if(GPXConstants.SRC_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node src found");
					trk.setSrc(getNodeValueAsString(currentNode));
				} else if(GPXConstants.LINK_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node link found");
					//TODO: parse link
					//trk.setLink(getNodeValueAsLink(currentNode));
				} else if(GPXConstants.NUMBER_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node number found");
					trk.setNumber(getNodeValueAsInteger(currentNode));
				} else if(GPXConstants.TYPE_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node type found");
					trk.setType(getNodeValueAsString(currentNode));
				} else if(GPXConstants.TRKSEG_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node trkseg found");
					trk.setTrackPoints(parseTrackSeg(currentNode));
				} else if(GPXConstants.EXTENSIONS_NODE.equals(currentNode.getNodeName())) {
					Iterator<IExtensionParser> it = extensionParsers.iterator();
					while(it.hasNext()) {
						if(logger.isDebugEnabled())logger.debug("node extensions found");
						while(it.hasNext()) {
							IExtensionParser parser = it.next();
							Object data = parser.parseTrackExtension(currentNode);
							trk.addExtensionData(parser.getId(), data);
						}
					}
				}
			}
		}
		
		return trk;
	}
	
	
	private Route parseRoute(Node node) {
		if(node == null) {
			logger.error("null node received");
			return null;
		}
		Route rte = new Route();
		NodeList nodes = node.getChildNodes();
		if(nodes != null) {
			for(int idx = 0; idx < nodes.getLength(); idx++) {
				Node currentNode = nodes.item(idx);
				if(GPXConstants.NAME_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node name found");
					rte.setName(getNodeValueAsString(currentNode));
				} else if(GPXConstants.CMT_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node cmt found");
					rte.setComment(getNodeValueAsString(currentNode));
				} else if(GPXConstants.DESC_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node desc found");
					rte.setDescription(getNodeValueAsString(currentNode));
				} else if(GPXConstants.SRC_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node src found");
					rte.setSrc(getNodeValueAsString(currentNode));
				} else if(GPXConstants.LINK_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node link found");
					//TODO: parse link
					//rte.setLink(getNodeValueAsLink(currentNode));
				} else if(GPXConstants.NUMBER_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node number found");
					rte.setNumber(getNodeValueAsInteger(currentNode));
				} else if(GPXConstants.TYPE_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node type found");
					rte.setType(getNodeValueAsString(currentNode));
				} else if(GPXConstants.RTEPT_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node rtept found");
					Waypoint wp = parseWaypoint(currentNode);
					if(wp!=null) {
						rte.addRoutePoint(wp);
					}
				} else if(GPXConstants.EXTENSIONS_NODE.equals(currentNode.getNodeName())) {
					Iterator<IExtensionParser> it = extensionParsers.iterator();
					while(it.hasNext()) {
						if(logger.isDebugEnabled())logger.debug("node extensions found");
						while(it.hasNext()) {
							IExtensionParser parser = it.next();
							Object data = parser.parseRouteExtension(currentNode);
							rte.addExtensionData(parser.getId(), data);
						}
					}
				}
			}
		}
		
		return rte;
	}
	
	private ArrayList<Waypoint> parseTrackSeg(Node node) {
		if(node == null) {
			logger.error("null node received");
			return null;
		}
		ArrayList<Waypoint> trkpts = new ArrayList<Waypoint>();

		NodeList nodes = node.getChildNodes();
		if(nodes != null) {
			for(int idx = 0; idx < nodes.getLength(); idx++) {
				Node currentNode = nodes.item(idx);
				if(GPXConstants.TRKPT_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node name found");
					Waypoint wp = parseWaypoint(currentNode);
					if(wp!=null) {
						trkpts.add(wp);
					}
				} else if(GPXConstants.EXTENSIONS_NODE.equals(currentNode.getNodeName())) {
					if(logger.isDebugEnabled())logger.debug("node extensions found");
					/*
					Iterator<IExtensionParser> it = extensionParsers.iterator();
					while(it.hasNext()) {
						IExtensionParser parser = it.next();
						Object data = parser.parseWaypointExtension(currentNode);
						//.addExtensionData(parser.getId(), data);
					}
					*/
				}
			}
		}
		return trkpts;
	}
	
	private Double getNodeValueAsDouble(Node node) {
		Double val = null;
		try {
			val = Double.parseDouble(node.getFirstChild().getNodeValue());
		} catch (Exception ex) {
			logger.error("error parsing Double value form node. val=" + node.getNodeValue(), ex);
		}
		return val;
	}
	
	private Date getNodeValueAsDate(Node node) {
		//2012-02-25T09:28:45Z
		Date val = null;
		try {
			
			val = sdf.parse(node.getFirstChild().getNodeValue());
		} catch (Exception ex) {
			logger.error("error parsing Date value form node. val=" + node.getNodeName(), ex);
		}
		return val;
	}
	
	private String getNodeValueAsString(Node node) {
		String val = null;
		try {
			val = node.getFirstChild().getNodeValue();
		} catch (Exception ex) {
			logger.error("error getting String value form node. val=" + node.getNodeName(), ex);
		}
		return val;
	}
	
	private FixType getNodeValueAsFixType(Node node) {
		FixType val = null;
		try {
			val = FixType.returnType(node.getFirstChild().getNodeValue());
		} catch (Exception ex) {
			logger.error("error getting FixType value form node. val=" + node.getNodeName(), ex);
		}
		return val;
	}
	
	private Integer getNodeValueAsInteger(Node node) {
		Integer val = null;
		try {
			val = Integer.parseInt(node.getFirstChild().getNodeValue());
		} catch (Exception ex) {
			logger.error("error parsing Integer value form node. val=" + node.getNodeValue(), ex);
		}
		return val;
	}
	
	public void writeGPX(GPX gpx, File gpxFile) throws IOException, ParserConfigurationException, TransformerException {
		OutputStream out = FileUtils.openOutputStream(gpxFile);
		writeGPX(gpx,out);
		out.flush();
		out.close();
	}
	
	public void writeGPX(GPX gpx, OutputStream out) throws ParserConfigurationException, TransformerException {
		DocumentBuilder builder = docFactory.newDocumentBuilder();
		Document doc = builder.newDocument();
		Node gpxNode = doc.createElement(GPXConstants.GPX_NODE);
		addBasicGPXInfoToNode(gpx, gpxNode, doc);
		if(gpx.getWaypoints() != null) {
			Iterator<Waypoint> itW = gpx.getWaypoints().iterator();
			while(itW.hasNext()) {
				addWaypointToGPXNode(itW.next(), gpxNode, doc);
			}
			Iterator<Track> itT = gpx.getTracks().iterator();
			while(itT.hasNext()) {
				addTrackToGPXNode(itT.next(), gpxNode, doc);
			}
			Iterator<Route> itR = gpx.getRoutes().iterator();
			while(itR.hasNext()) {
				addRouteToGPXNode(itR.next(), gpxNode, doc);
			}
		}
		
		doc.appendChild(gpxNode);
		
		// Use a Transformer for output
		
		Transformer transformer = tFactory.newTransformer();
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
		DOMSource source = new DOMSource(doc);
		StreamResult result = new StreamResult(out);
		transformer.transform(source, result); 
	}
	
	private void addWaypointToGPXNode(Waypoint wpt, Node gpxNode, Document doc) {
		addGenericWaypointToGPXNode(GPXConstants.WPT_NODE, wpt, gpxNode, doc);
	}
	
	private void addGenericWaypointToGPXNode(String tagName,Waypoint wpt, Node gpxNode, Document doc) {
		Node wptNode = doc.createElement(tagName);
		NamedNodeMap attrs = wptNode.getAttributes();
		if(wpt.getLatitude() != null) {
			Node latNode = doc.createAttribute(GPXConstants.LAT_ATTR);
			latNode.setNodeValue(wpt.getLatitude().toString());
			attrs.setNamedItem(latNode);
		}
		if(wpt.getLongitude() != null) {
			Node longNode = doc.createAttribute(GPXConstants.LON_ATTR);
			longNode.setNodeValue(wpt.getLongitude().toString());
			attrs.setNamedItem(longNode);
		}
		if(wpt.getElevation() != null) {
			Node node = doc.createElement(GPXConstants.ELE_NODE);
			node.appendChild(doc.createTextNode(wpt.getElevation().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getTime() != null) {
			Node node = doc.createElement(GPXConstants.TIME_NODE);
			node.appendChild(doc.createTextNode(sdfZ.format(wpt.getTime())));
			wptNode.appendChild(node);
		}
		if(wpt.getMagneticDeclination() != null) {
			Node node = doc.createElement(GPXConstants.MAGVAR_NODE);
			node.appendChild(doc.createTextNode(wpt.getMagneticDeclination().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getGeoidHeight() != null) {
			Node node = doc.createElement(GPXConstants.GEOIDHEIGHT_NODE);
			node.appendChild(doc.createTextNode(wpt.getGeoidHeight().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getName() != null) {
			Node node = doc.createElement(GPXConstants.NAME_NODE);
			node.appendChild(doc.createTextNode(wpt.getName()));
			wptNode.appendChild(node);
		}
		if(wpt.getComment() != null) {
			Node node = doc.createElement(GPXConstants.CMT_NODE);
			node.appendChild(doc.createTextNode(wpt.getComment()));
			wptNode.appendChild(node);
		}
		if(wpt.getDescription() != null) {
			Node node = doc.createElement(GPXConstants.DESC_NODE);
			node.appendChild(doc.createTextNode(wpt.getDescription()));
			wptNode.appendChild(node);
		}
		if(wpt.getSrc() != null) {
			Node node = doc.createElement(GPXConstants.SRC_NODE);
			node.appendChild(doc.createTextNode(wpt.getSrc()));
			wptNode.appendChild(node);
		}
		//TODO: write link node
		if(wpt.getSym() != null) {
			Node node = doc.createElement(GPXConstants.SYM_NODE);
			node.appendChild(doc.createTextNode(wpt.getSym()));
			wptNode.appendChild(node);
		}
		if(wpt.getType() != null) {
			Node node = doc.createElement(GPXConstants.TYPE_NODE);
			node.appendChild(doc.createTextNode(wpt.getType()));
			wptNode.appendChild(node);
		}
		if(wpt.getFix() != null) {
			Node node = doc.createElement(GPXConstants.FIX_NODE);
			node.appendChild(doc.createTextNode(wpt.getFix().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getSat() != null) {
			Node node = doc.createElement(GPXConstants.SAT_NODE);
			node.appendChild(doc.createTextNode(wpt.getSat().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getHdop() != null) {
			Node node = doc.createElement(GPXConstants.HDOP_NODE);
			node.appendChild(doc.createTextNode(wpt.getHdop().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getVdop() != null) {
			Node node = doc.createElement(GPXConstants.VDOP_NODE);
			node.appendChild(doc.createTextNode(wpt.getVdop().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getPdop() != null) {
			Node node = doc.createElement(GPXConstants.PDOP_NODE);
			node.appendChild(doc.createTextNode(wpt.getPdop().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getAgeOfGPSData() != null) {
			Node node = doc.createElement(GPXConstants.AGEOFGPSDATA_NODE);
			node.appendChild(doc.createTextNode(wpt.getAgeOfGPSData().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getDgpsid() != null) {
			Node node = doc.createElement(GPXConstants.DGPSID_NODE);
			node.appendChild(doc.createTextNode(wpt.getDgpsid().toString()));
			wptNode.appendChild(node);
		}
		if(wpt.getExtensionsParsed() > 0) {
			Node node = doc.createElement(GPXConstants.EXTENSIONS_NODE);
			Iterator<IExtensionParser> it = extensionParsers.iterator();
			while(it.hasNext()) {
				it.next().writeWaypointExtensionData(node, wpt, doc);
			}
			wptNode.appendChild(node);
		}
		gpxNode.appendChild(wptNode);
	}

	private void addTrackToGPXNode(Track trk, Node gpxNode, Document doc) {
		Node trkNode = doc.createElement(GPXConstants.TRK_NODE);
		
		if(trk.getName() != null) {
			Node node = doc.createElement(GPXConstants.NAME_NODE);
			node.appendChild(doc.createTextNode(trk.getName()));
			trkNode.appendChild(node);
		}
		if(trk.getComment() != null) {
			Node node = doc.createElement(GPXConstants.CMT_NODE);
			node.appendChild(doc.createTextNode(trk.getComment()));
			trkNode.appendChild(node);
		}
		if(trk.getDescription() != null) {
			Node node = doc.createElement(GPXConstants.DESC_NODE);
			node.appendChild(doc.createTextNode(trk.getDescription()));
			trkNode.appendChild(node);
		}
		if(trk.getSrc() != null) {
			Node node = doc.createElement(GPXConstants.SRC_NODE);
			node.appendChild(doc.createTextNode(trk.getSrc()));
			trkNode.appendChild(node);
		}
		//TODO: write link
		if(trk.getNumber() != null) {
			Node node = doc.createElement(GPXConstants.NUMBER_NODE);
			node.appendChild(doc.createTextNode(trk.getNumber().toString()));
			trkNode.appendChild(node);
		}
		if(trk.getType() != null) {
			Node node = doc.createElement(GPXConstants.TYPE_NODE);
			node.appendChild(doc.createTextNode(trk.getType()));
			trkNode.appendChild(node);
		}
		if(trk.getExtensionsParsed() > 0) {
			Node node = doc.createElement(GPXConstants.EXTENSIONS_NODE);
			Iterator<IExtensionParser> it = extensionParsers.iterator();
			while(it.hasNext()) {
				it.next().writeTrackExtensionData(node, trk, doc);
			}
			trkNode.appendChild(node);
		}
		if(trk.getTrackPoints() != null) {
			Node trksegNode = doc.createElement(GPXConstants.TRKSEG_NODE);
			Iterator<Waypoint> it = trk.getTrackPoints().iterator();
			while(it.hasNext()) {
				addGenericWaypointToGPXNode(GPXConstants.TRKPT_NODE, it.next(), trksegNode, doc);
			}
			trkNode.appendChild(trksegNode);
		}
		gpxNode.appendChild(trkNode);
	}
	
	private void addRouteToGPXNode(Route rte, Node gpxNode, Document doc) {
		Node trkNode = doc.createElement(GPXConstants.TRK_NODE);
		
		if(rte.getName() != null) {
			Node node = doc.createElement(GPXConstants.NAME_NODE);
			node.appendChild(doc.createTextNode(rte.getName()));
			trkNode.appendChild(node);
		}
		if(rte.getComment() != null) {
			Node node = doc.createElement(GPXConstants.CMT_NODE);
			node.appendChild(doc.createTextNode(rte.getComment()));
			trkNode.appendChild(node);
		}
		if(rte.getDescription() != null) {
			Node node = doc.createElement(GPXConstants.DESC_NODE);
			node.appendChild(doc.createTextNode(rte.getDescription()));
			trkNode.appendChild(node);
		}
		if(rte.getSrc() != null) {
			Node node = doc.createElement(GPXConstants.SRC_NODE);
			node.appendChild(doc.createTextNode(rte.getSrc()));
			trkNode.appendChild(node);
		}
		//TODO: write link
		if(rte.getNumber() != null) {
			Node node = doc.createElement(GPXConstants.NUMBER_NODE);
			node.appendChild(doc.createTextNode(rte.getNumber().toString()));
			trkNode.appendChild(node);
		}
		if(rte.getType() != null) {
			Node node = doc.createElement(GPXConstants.TYPE_NODE);
			node.appendChild(doc.createTextNode(rte.getType()));
			trkNode.appendChild(node);
		}
		if(rte.getExtensionsParsed() > 0) {
			Node node = doc.createElement(GPXConstants.EXTENSIONS_NODE);
			Iterator<IExtensionParser> it = extensionParsers.iterator();
			while(it.hasNext()) {
				it.next().writeRouteExtensionData(node, rte, doc);
			}
			trkNode.appendChild(node);
		}
		if(rte.getRoutePoints() != null) {
			Iterator<Waypoint> it = rte.getRoutePoints().iterator();
			while(it.hasNext()) {
				addGenericWaypointToGPXNode(GPXConstants.RTEPT_NODE, it.next(), trkNode, doc);
			}
		}
		gpxNode.appendChild(trkNode);
	}
	
	private void addBasicGPXInfoToNode(GPX gpx, Node gpxNode, Document doc) {
		NamedNodeMap attrs = gpxNode.getAttributes();
		if(gpx.getVersion() != null) {
			Node verNode = doc.createAttribute(GPXConstants.VERSION_ATTR);
			verNode.setNodeValue(gpx.getVersion());
			attrs.setNamedItem(verNode);
		}
		if(gpx.getCreator() != null) {
			Node creatorNode = doc.createAttribute(GPXConstants.CREATOR_ATTR);
			creatorNode.setNodeValue(gpx.getCreator());
			attrs.setNamedItem(creatorNode);
		}
		
		if(gpx.getExtensionsParsed() > 0) {
			Node node = doc.createElement(GPXConstants.EXTENSIONS_NODE);
			Iterator<IExtensionParser> it = extensionParsers.iterator();
			while(it.hasNext()) {
				it.next().writeGPXExtensionData(node, gpx, doc);
			}
			gpxNode.appendChild(node);
		}
	}
}