@Deprecated
public class ConvertGpx {
	private static final @Nonnull Log log = Log.getInstance();

	public Document generateNikePlusGpx(File tcxFile) throws ConverterException {
		try {
			Document tcxDoc = Util.generateDocument(tcxFile);
			return generateNikePlusGpx(tcxDoc);
		} catch (Throwable throwable) {
			throw new ConverterException(throwable.getMessage(), throwable);
		}
	}

	public Document generateNikePlusGpx(Document inDoc) throws ConverterException {
		try {
			// Create output document
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document outDoc = db.newDocument();

			// gpx (root)
			Element gpxElement = Util.appendElement(outDoc, "gpx", null, "xmlns", "http://www.topografix.com/GPX/1/1", "creator", "NikePlus", "version", "1.1");

			// trk
			Element trk = Util.appendElement(gpxElement, "trk");
			Util.appendCDATASection(trk, "name", "4c888a06");
			Util.appendCDATASection(trk, "desc", "workout");

			// trkseg
			Element trkSeg = Util.appendElement(trk, "trkseg");

			// trkpt's
			appendTrkptElements(inDoc, trkSeg);

			return outDoc;
		} catch (Throwable throwable) {
			throw new ConverterException(throwable.getMessage(), throwable);
		}
	}


	/**
	 * Appends all the <trkrpt> data to the output xml.
	 * <p>
	 * I have chosen to iterate through the garmin data and append rather than copy it all over then iterate
	 * through it and remove unrequired data.  The reason for this is that if garmin decided to modify the gpx
	 * schema I do not have to worry about modifying my code to remove additional elements.
	 * At the time of writing (2010-09-13) nike+ use gpx data as follows:
	 * <pre>
	 * {@code
	 * <trkpt lat="51.7944969579" lon="-0.0769600122">
	 *   <ele>50.5970001221</ele>
	 *   <time>2010-09-09T07:18:04Z</time>
	 * </trkpt>
	 * }
	 * </pre>
	 * @param inDoc The garmin gpx document we are reading the data from.
	 * @param trkSeg The xml element in the output document to which we are appending the data.
	 */
	private void appendTrkptElements(Document inDoc, Element trkSeg) {
		
		NodeList trkpts = inDoc.getElementsByTagName("trkpt");

		int trkptsLength = trkpts.getLength();

		for (int i = 0; i < trkptsLength; ++i) {
			Node trkptGarmin = trkpts.item(i);
			NamedNodeMap latlon = trkptGarmin.getAttributes();
			String lon = Util.getSimpleNodeValue(latlon.getNamedItem("lon"));
			String lat = Util.getSimpleNodeValue(latlon.getNamedItem("lat"));

			Element trkptNike = Util.appendElement(trkSeg, "trkpt", null, "lat", lat, "lon", lon);

			for (Node n = trkptGarmin.getFirstChild(); n != null; n = n.getNextSibling()) {
				String nodeName = n.getNodeName();

				if ((nodeName.equals("ele")) || (nodeName.equals("time")))
					Util.appendElement(trkptNike, nodeName, Util.getSimpleNodeValue(n));
			}
		}
	}


	public static void main(String[] args) {
		File inFile = new File(args[0]);

		ConvertGpx c = new ConvertGpx();
		try {
			Document doc = c.generateNikePlusGpx(inFile);
			Util.writeDocument(doc, "gpxXML.xml");
		}
		catch (Throwable t) {
			log.out(t);
		}
	}	
} @Deprecated
public class ConvertTcx
{

	private static final double				D_METRES_PER_MILE = 1609.344;
	private static final double				D_METRES_PER_MILE_GARMIN = 1609.35;		// Garmin use this as their DistanceMeters for laps of length 1 mile.
	private static final int				MILLIS_PER_HOUR = 60 * 1000 * 60;
	private static final String				DATE_TIME_FORMAT_NIKE = "yyyy-MM-dd'T'HH:mm:ssZ";
	private static final String				DATE_TIME_FORMAT_HUMAN = "d MMM yyyy HH:mm:ss z";

	// The minimum amount of millis between Trackpoints that I am prepared to create a potential pause/resume for.
	private static final int				MILLIS_POTENTIAL_PAUSE_THRESHOLD = 0;


	private long _totalDuration;
	private double _totalDistance;
	private boolean _forceExcludeHeartRateData;
	private boolean _includeHeartRateData = false;

	private TimeZone _workoutTimeZone;
	private Calendar _calStart;
	private Calendar _calEnd;
	private String _startTimeString;
	private String _startTimeStringHuman;

	private static final Log log = Log.getInstance();


	/**
	 * Converts a garmin tcx file to a nike+ workout xml document.
	 * <p>
	 * This class is a hacky mess just now but it does the job.
	 * @author angus
	 */
	public ConvertTcx() {
	}


	public Document generateNikePlusXml(File tcxFile, String empedID) throws Throwable {
		return generateNikePlusXml(tcxFile, empedID, false);
	}


	public Document generateNikePlusXml(File tcxFile, String empedID, boolean forceExcludeHeartRateData) throws Throwable {
		//try {
			_forceExcludeHeartRateData = forceExcludeHeartRateData;
			Document tcxDoc = Util.generateDocument(tcxFile);
			return generateNikePlusXml(tcxDoc, empedID);
		//}
		//catch (Exception e) {
		//	log.out(e);
		//	return null;
		//}
	}


	public Document generateNikePlusXml(Document inDoc, String empedID) throws ConverterException {
		return generateNikePlusXml(inDoc, empedID, null);
	}


	public Document generateNikePlusXml(Document inDoc,
										String empedID,
										Integer clientTimeZoneOffset) throws ConverterException {
		return generateNikePlusXml(inDoc, empedID, clientTimeZoneOffset, false);
	}

	public Document generateNikePlusXml(Document inDoc,
										String empedID,
										Integer clientTimeZoneOffset, boolean forceExcludeHeartRateData) throws ConverterException {
		_forceExcludeHeartRateData = forceExcludeHeartRateData;
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

		try {
			// Create output document
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document outDoc = db.newDocument();
			
			// Sports Data (root)
			Element sportsDataElement = Util.appendElement(outDoc, "sportsData");

			// Vers
			//Util.appendElement(sportsDataElement, "vers", "8");				// 2010-09-13: I've noticed this is not included in the iphone xml output.

			// Run Summary
			ArrayList<Double> onDemandVPDistances = new ArrayList<Double>();
			Element runSummary = appendRunSummary(inDoc, sportsDataElement, clientTimeZoneOffset, onDemandVPDistances);

			// Template
			appendTemplate(sportsDataElement);

			// Goal Type
			appendGoalType(sportsDataElement);

			// User Info
			appendUserInfo(inDoc, sportsDataElement, empedID);

			// Start Time
			Util.appendElement(sportsDataElement, "startTime", _startTimeString);

			
			// Workout Detail
			appendWorkoutDetail(inDoc, sportsDataElement, runSummary, onDemandVPDistances);

			// Test lap durations
			//testLapDuration(inDoc);

			//printDocument(outDoc);
			//writeDocument(outDoc);
			//return new String[] { generateString(outDoc), generateFileName() };
			
			return outDoc;
		} catch (Throwable throwable) {
			throw new ConverterException(throwable.getMessage(), throwable);
		}
	}



	/*
	*/
	/**
	 * Generates a run summary xml element like:
	 * <pre>
	 * {@code
	 * <runSummary>
	 *   <workoutName><![CDATA[Basic]]></workoutName>
	 *   <time>2009-06-12T10:00:00-10:00</time>
	 *   <duration>1760000</duration>
	 *   <durationString>29:20</durationString>
	 *   <distance unit="km">6.47</distance>
	 *   <distanceString>6.47 km</distanceString>
	 *   <pace>4:32 min/km</pace>
	 *   <calories>505</calories>
	 *   <battery></battery>
	 *   <stepCounts>
	 *     <walkBegin>0</walkBegin>
	 *     <walkEnd>0</walkEnd>
	 *     <runBegin>0</runBegin>
	 *     <runEnd>0</runEnd>
	 *   </stepCounts>
	 * </runSummary>
	 * }
	 * </pre>
	 * @param inDoc The garmin tcx document we are reading the data from.
	 * @param sportsDataElement An xml element we have already created "sportsDataElement"
	 * @param clientTimeZoneOffset The client timezone offset to use in the event we are unable to determine workout timezone from the gps coordinates.
	 * @return The runSummary xml element.
	 * @throws DatatypeConfigurationException
	 * @throws IOException
	 * @throws MalformedURLException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 */
	private Element appendRunSummary(Document inDoc, Element sportsDataElement, Integer clientTimeZoneOffset, ArrayList<Double> onDemandVPDistances) throws DatatypeConfigurationException, IOException, MalformedURLException, ParserConfigurationException, SAXException {
		Element runSummaryElement = Util.appendElement(sportsDataElement, "runSummary");

		// Workout Name
		//Util.appendCDATASection(runSummaryElement, "workoutName", "Basic");		// 2010-09-13: I've noticed this is not included in the iphone xml output.

		// Start Time
		appendStartTime(inDoc, runSummaryElement, clientTimeZoneOffset);

		// Duration, Duration, Pace & Calories
		appendTotals(inDoc, runSummaryElement, onDemandVPDistances);

		// Battery
		Util.appendElement(runSummaryElement, "battery");

		// Step Counts
		//appendStepCounts(runSummaryElement);									// 2010-09-13: I've noticed this is not included in the iphone xml output.

		return runSummaryElement;
	}




	private void appendStartTime(Document inDoc, Element runSummaryElement, Integer clientTimeZoneOffset) throws DatatypeConfigurationException, IOException, MalformedURLException, ParserConfigurationException, SAXException {

		// Garmin:	 2009-06-03T16:59:27.000Z
		// Nike:	 2009-06-03T17:59:27+01:00
		
		// Get the timezone based on the Latitude, Longitude & Time (UTC) of the first TrackPoint
		_workoutTimeZone = getWorkoutTimeZone(inDoc, clientTimeZoneOffset);
		if (_workoutTimeZone == null)
			_workoutTimeZone = TimeZone.getTimeZone("Etc/UTC");

		log.out("Time zone millis offset (vs server): %d", _workoutTimeZone.getRawOffset());

		// Set the the SimpleDateFormat object so that it prints the time correctly:
		// local-time + UTC-difference.
		SimpleDateFormat df = new SimpleDateFormat(DATE_TIME_FORMAT_NIKE);
		df.setTimeZone(_workoutTimeZone);
		
		// Get the workout start-time, format for nike+ and add it to the out-document
		Calendar calStart = Util.getCalendarValue(Util.getSimpleNodeValue(inDoc, "Id"));
		Date dateStart = calStart.getTime();
		_startTimeString = df.format(dateStart);
		_startTimeString = String.format("%s:%s", _startTimeString.substring(0, 22), _startTimeString.substring(22));
		Util.appendElement(runSummaryElement, "time", _startTimeString);

		// Generate a human readable start-string we can use for debugging.
		df.applyPattern(DATE_TIME_FORMAT_HUMAN);
		_startTimeStringHuman = df.format(dateStart);

		// We need these later.
		_calStart = calStart;
	}



	// We use the latitude & longitude data along with the http://ws.geonames.org/timezone webservice to
	// deduce in which time zone the workout began.
	private TimeZone getWorkoutTimeZone(Document inDoc, Integer clientTimeZoneOffset) throws DatatypeConfigurationException, IOException, MalformedURLException, ParserConfigurationException, SAXException {
		NodeList positions = inDoc.getElementsByTagName("Position");
		int positionsLength = positions.getLength();

		// Loop through the Position data, we will return as soon as we find one
		// with the required data (latitude & longitude).
		for (int i = 0; i < positionsLength; ++i) {

			Double latitude = null;
			Double longitude = null;

			for (Node n = positions.item(i).getFirstChild(); n != null; n = n.getNextSibling()) {
				String nodeName = n.getNodeName();

				// Latitude at this point
				if (nodeName.equals("LatitudeDegrees")) {
					latitude = Double.parseDouble(Util.getSimpleNodeValue(n));
				}

				// Longitude at this point
				else if (nodeName.equals("LongitudeDegrees")) {
					longitude = Double.parseDouble(Util.getSimpleNodeValue(n));
				}


				if ((latitude != null) && (longitude != null)) {
					// Send a post to the geonames.org webservice with the lat & lng parameters.
					String url = "http://ws.geonames.org/timezone";
					String parameters = String.format("&username=tcx2nikeplus_app&lat=%s&lng=%s", latitude, longitude);
					
					log.out("Looking up time zone for lat/lon: %.4f / %.4f", latitude, longitude);
					try {
						Document response = Util.downloadFile(url, parameters);
						String timeZoneId = Util.getSimpleNodeValue(response, "timezoneId");
						log.out(" - %s", timeZoneId);
						return TimeZone.getTimeZone(timeZoneId);
					}
					catch (Throwable t) {

						// If, for whatever reason we are unable to get the timezone and we have a clientTimeZoneOffset then use that.
						if (clientTimeZoneOffset != null) {
							int hours = clientTimeZoneOffset / 60;
							int minutes = Math.abs(clientTimeZoneOffset) % 60;
							String mm = String.format(((minutes < 10) ? "0%d" : "%d"), minutes);
							String tz = String.format("GMT%s%d:%s", (hours < 0) ? "" : "+", hours, mm);

							log.out("Unable to retrieve workout timezone from http://ws.geonames.org, attempting to use client-timezone %s instead.", tz);
							return TimeZone.getTimeZone(tz);
						}
						else {
							//throw new IOException("Unable to retrieve workout timezone (http://ws.geonames.org is not available right now).  Please try again later.", t);
							TimeZone tz = TimeZone.getDefault();
							log.out(Level.WARNING, "Unable to retrieve workout timezone (http://ws.geonames.org is not available right now).  Using default timezone: %s", tz.getID());
							return tz;
						}
					}
				}
			}
		}

		return null;
	}



	private void appendTotals(Document inDoc, Node parent, ArrayList<Double> onDemandVPDistances) {
		double totalSeconds = 0d;
		double totalDistance = 0d;
		int totalCalories = 0;

		NodeList laps = inDoc.getElementsByTagName("Lap");

		int lapsLength = laps.getLength();

		for (int i = 0; i < lapsLength; ++i) {

			for (Node n = laps.item(i).getFirstChild(); n != null; n = n.getNextSibling()) {

				String nodeName = n.getNodeName();

				if (nodeName.equals("TotalTimeSeconds")) 
					totalSeconds += Double.parseDouble(Util.getSimpleNodeValue(n));
				
				else if (nodeName.equals("DistanceMeters")) {
					totalDistance += Double.parseDouble(Util.getSimpleNodeValue(n));
					// If the end of the lap does not fall on a km/mile distance then store for inserting as a "onDemandVP" click later.
					if (((totalDistance % 1000) != 0) && (totalDistance % D_METRES_PER_MILE_GARMIN) != 0)
						onDemandVPDistances.add(totalDistance);
				}

				else if (nodeName.equals("Calories"))
					totalCalories += Integer.parseInt(Util.getSimpleNodeValue(n));
			}
		}


		long totalDuration = (long) (totalSeconds * 1000);

		_totalDuration = totalDuration;
		_totalDistance = totalDistance;

		// Calculate the end-time of the run (for use late to generate the xml filename).
		_calEnd = (Calendar)(_calStart.clone());
		_calEnd.add(Calendar.MILLISECOND, (int) totalDuration);

		// Total Duration
		Util.appendElement(parent, "duration", totalDuration);
		//Util.appendElement(parent, "durationString", getTimeStringFromMillis(totalDuration));		// 2010-09-13: I've noticed this is not included in the iphone xml output.

		// Total Distance
		//totalDistance = totalDistance.divide(BD_1000);
		totalDistance /= 1000;
		Util.appendElement(parent, "distance", String.format("%.4f", totalDistance), "unit", "km");
		//Util.appendElement(parent, "distanceString", String.format("%.2f km", totalDistance));		// 2010-09-13: I've noticed this is not included in the iphone xml output.

		// Pace
		//Util.appendElement(parent, "pace", String.format("%s min/km", getTimeStringFromMillis(calculatePace(totalDuration, totalDistance))));		// 2010-09-13: I've noticed this is not included in the iphone xml output.

		// Calories - FIXME - calories are not calculated properly yet...
		Util.appendElement(parent, "calories", totalCalories);
	}


	/*
	// 2010-09-13: I've noticed this is not included in the iphone xml output.
	private void appendStepCounts(Node parent) {
		Element stepCountsElement = Util.appendElement(parent, "stepCounts");

		Util.appendElement(stepCountsElement, "walkBegin", "0");
		Util.appendElement(stepCountsElement, "walkEnd", "0");
		Util.appendElement(stepCountsElement, "runBegin", "0");
		Util.appendElement(stepCountsElement, "runEnd", "0");
	}
	*/

	/*
	<template><templateID>8D495DCE</templateID>
	<templateName><![CDATA[Basic]]></templateName>
	</template>
	*/
	private void appendTemplate(Element sportsDataElement) {
		Element templateElement = Util.appendElement(sportsDataElement, "template");
		//Util.appendElement(templateElement, "templateID", "8D495DCE");					// 2010-09-13: I've noticed this is not included in the iphone xml output.
		Util.appendCDATASection(templateElement, "templateName", "Basic");
	}


	// <goal type="" value="" unit=""></goal>
	private void appendGoalType(Element sportsDataElement) {
		Util.appendElement(sportsDataElement, "goal", null, "type", "", "value", "", "unit", "");
	}

	/*
	<userInfo>
	  <empedID>XXXXXXXXXXX</empedID>
	  <weight></weight>
	  <device>iPod</device>
	  <calibration></calibration>
	</userInfo>
	*/
	private void appendUserInfo(Document inDoc, Element sportsDataElement, String empedID) {
		Element templateElement = Util.appendElement(sportsDataElement, "userInfo");
		Util.appendElement(templateElement, "empedID", (empedID == null) ? "XXXXXXXXXXX" : empedID);
		Util.appendElement(templateElement, "weight");

		// Upload goes through fine when "Garmin Forerunner 405 is specified as device but on nikeplus.com website some data shows as "invalid"
		//Util.appendElement(templateElement, "device",	getSimpleNodeValue(inDoc, "Name"));		// Garmin Forerunner 405
		Util.appendElement(templateElement, "device", "iPod");								// iPod
		Util.appendElement(templateElement, "calibration");
	}

	


	private void appendWorkoutDetail(Document inDoc, Element sportsDataElement, Element runSummaryElement, ArrayList<Double> onDemandVPDistances) throws DatatypeConfigurationException, ConverterException {

		// Generate the workout detail from the garmin Trackpoint data.
		ArrayList<Trackpoint> trackpoints = new ArrayList<Trackpoint>();
		ArrayList<Long> pauseResumeTimes = new ArrayList<Long>();
		generateSplineData(inDoc, trackpoints, pauseResumeTimes);

		// Generates the following PolynomialSplineFunctions: distanceToDuration, durationToDistance, durationToPace, durationToHeartRate.

		PolynomialSplineFunction[] splines = generateSplineFunctions(trackpoints);
		appendSnapShotListAndExtendedData(sportsDataElement, onDemandVPDistances, pauseResumeTimes, splines[0], splines[1], splines[2], splines[3]);
		
		// Append heart rate detail to the run summary if required.
		if (_includeHeartRateData) {

			Trackpoint min = null;
			Trackpoint max = null;
			int average = 0;

			for (Trackpoint tp : trackpoints) {
				int heartRate = tp.getHeartRate().intValue();
				if ((min == null) || (heartRate < min.getHeartRate())) min = tp;
				if ((max == null) || (heartRate > max.getHeartRate())) max = tp;
				average += (heartRate * tp.getDurationSinceLastTrackpoint());
			}

			average /= _totalDuration;

			Element heartRateElement = Util.appendElement(runSummaryElement, "heartRate");
			Util.appendElement(heartRateElement, "average", average);
			appendHeartRateSummary(heartRateElement, "minimum", min);
			appendHeartRateSummary(heartRateElement, "maximum", max);
			//Util.appendElement(heartRateElement, "battery", 3);							// 2010-09-13: I've noticed this is not included in the iphone xml output.
		}
		
	}

	private void appendHeartRateSummary(Element heartRateElement, String type, Trackpoint tp) {
		Element heartRateTypeElement = Util.appendElement(heartRateElement, type);
		Util.appendElement(heartRateTypeElement, "duration", tp.getDuration());
		Util.appendElement(heartRateTypeElement, "distance", String.format("%.3f", tp.getDistance()/1000));
		Util.appendElement(heartRateTypeElement, "pace", tp.getPace());
		Util.appendElement(heartRateTypeElement, "bpm", tp.getHeartRate().intValue());
	}



	private void generateSplineData(Document inDoc, ArrayList<Trackpoint> trackpointsStore, ArrayList<Long> pauseResumeTimes) throws DatatypeConfigurationException {
		
		// Create a trackpoint based on the very start of the run.
		Trackpoint previousTp = new Trackpoint(0l, 0d, 0d, null);
		trackpointsStore.add(previousTp);

		NodeList laps = inDoc.getElementsByTagName("Lap");
		int lapsLength = laps.getLength();
		long lapStartDuration = 0;

		for (int i = 0; i < lapsLength; ++i) {
			Node lap = laps.item(i);
			long lapStartTime = Util.getCalendarNodeValue(lap.getAttributes().getNamedItem("StartTime")).getTimeInMillis();
			long lapDuration = (long) Double.parseDouble(Util.getSimpleNodeValue(Util.getFirstChildByNodeName(lap, "TotalTimeSeconds"))) * 1000;

			log.out(Level.FINE, "Start of lap %d\tDuration: %d -> %d.", (i + 1), lapStartDuration, (lapStartDuration + lapDuration));

			ArrayList<Trackpoint> lapTrackpointStore = generateLapSplineData(lap, pauseResumeTimes, getLastTrackpointFromArrayList(trackpointsStore), lapStartTime, lapStartDuration, lapDuration);
			trackpointsStore.addAll(lapTrackpointStore);

			lapStartDuration += lapDuration;
			log.out(Level.FINE, ("End of lap.\n"));
		}


		// Remove strange trackpoints (repeat distance/durations).
		// Also, for each trackpoint, add a heart-rate reading if one is required & update the previous-trackpoints reference.
		previousTp = null;

		Iterator<Trackpoint> tpsIt = trackpointsStore.iterator();
		while (tpsIt.hasNext()) {
			Trackpoint tp = tpsIt.next();
			tp.setPreviousTrackpoint(previousTp);

			if ((tp.getDistance() == null) || (tp.isRepeatDistance())) {
				log.out(Level.FINEST, "Removing invalid distance trackpoint:\t%s", tp);
				tpsIt.remove();
			}
			else if ((tp.getDuration() == null) || (tp.isRepeatDuration())) {
				log.out(Level.FINEST, "Removing invalid duration trackpoint:\t%s", tp);
				tpsIt.remove();
			}
			else {
				if ((_includeHeartRateData) && (tp.getHeartRate() == null))
					tp.setHeartRate(previousTp.getHeartRate());

				previousTp = tp;
				log.out(Level.FINEST, "Duration: %d\tDistance: %.4f", tp.getDuration(), tp.getDistance());
			}
		}

		//log.out(Level.FINER, "Workout total duration: %d", _totalDuration);
		Trackpoint penultimateTp = getTrackpointFromEndofArrayList(trackpointsStore, 1);
		if (penultimateTp != null) {
			long pDur = penultimateTp.getDuration();
			double pDist = penultimateTp.getDistance();
			log.out("Trackpoint reading complete...");
			log.out("Penultimate vs Required:  Duration (secs): %d -> %d (%d).  Distance (m): %.0f -> %.0f (%.0f)",
				pDur/1000, _totalDuration/1000, ((_totalDuration - pDur)/1000), pDist, _totalDistance, (_totalDistance - pDist));
		}

	}


	private ArrayList<Trackpoint> generateLapSplineData(Node lap, ArrayList<Long> pauseResumeTimes, Trackpoint previousTp, long lapStartTime, long lapStartDuration, long lapDuration) throws DatatypeConfigurationException {

		ArrayList<Trackpoint> lapTrackpointStore = new ArrayList<Trackpoint>();
		// Add the last trackpoint of the previous lap so we can calculate pause/resumes that span across laps.
		// We will remove the first trackpoint (this one) from the store after validating the lap data.
		lapTrackpointStore.add(previousTp);

		long lapEndDuration = lapStartDuration + lapDuration;

		double lapDistance = Double.parseDouble(Util.getSimpleNodeValue(Util.getFirstChildByNodeName(lap, "DistanceMeters")));
		Double lapEndDistance = null;

		// Get the trackpoints for this lap - if there are none then continue to the next lap.
		Node[] tracks = Util.getChildrenByNodeName(lap, "Track");

		if (tracks != null) {
			for (Node track : tracks) {
				// Get the trackpoints for this track - if there are none then continue to the next track.
				Node[] trackpoints = Util.getChildrenByNodeName(track, "Trackpoint");
				if (trackpoints == null) continue;

				for (Node trackpoint : trackpoints) {

					// Loop through the data for this trackpoint storing the data.
					Trackpoint tp = new Trackpoint(previousTp);
					int trackPointDataLength = 0;

					for (Node n = trackpoint.getFirstChild(); n != null; n = n.getNextSibling(), ++trackPointDataLength) {
						String nodeName = n.getNodeName();

						// Run duration to this point
						if (nodeName.equals("Time"))
							tp.setDuration((Util.getCalendarNodeValue(n).getTimeInMillis() - lapStartTime) + lapStartDuration);

						// Distance to this point
						else if (nodeName.equals("DistanceMeters")) {
							double distance = Double.parseDouble(Util.getSimpleNodeValue(n));

							// If this is the first trackpoint in the lap with a DistanceMeters value then calculate the start/end distance of the lap.
							if (lapEndDistance == null) lapEndDistance = distance + lapDistance;

							// Only add trackpoints where the distance is greater than the most recent distance (other values, such as a load of
							// 0.00m at the start of a workout indicate no satellites (I think).
							// We discard the trackpoint here, otherwise it'll be used in the lap validation as a 'slowest pace' trackpoint when it shouldn't be.
							if (distance > previousTp.getMostRecentDistance())
								tp.setDistance(distance);
						}

						// Heart rate bpm
						else if ((!_forceExcludeHeartRateData) && (nodeName.equals("HeartRateBpm"))) {
							NodeList heartRateData = n.getChildNodes();
							int heartRateDataLength = heartRateData.getLength();

								for (int j = 0; j < heartRateDataLength; ++j) {
								Node heartRateNode = heartRateData.item(j);
								if (heartRateNode.getNodeName().equals("Value")) {
									_includeHeartRateData = true;
									tp.setHeartRate(Double.parseDouble(Util.getSimpleNodeValue(heartRateNode)));
								}
							}
						}
					}

					// Store the trackpoint for validation/conversion later.
					if ((tp != null) && ((tp.getDistance() != null) || (trackPointDataLength == 3))) {
						lapTrackpointStore.add(tp);
						previousTp = tp;

						log.out(Level.FINEST, "New raw tcx-file Trackpoint, durations: %d -> %d = %d.  distance: %.4f.", tp.getPreviousDuration(), tp.getDuration(), tp.getDuration() - tp.getPreviousDuration(), tp.getDistance());
					}
				}
			}
		}

		// Add a Trackpoint for the end of the lap if we haven't already got one at that distance/duration.
		Trackpoint lastTp = getLastTrackpointFromArrayList(lapTrackpointStore);
		if ((lapEndDistance != null) && ((lastTp.getMostRecentDistance() < lapEndDistance) && (lastTp.getDuration() < lapEndDuration)))
			lapTrackpointStore.add(new Trackpoint(lapEndDuration, lapEndDistance, previousTp.getHeartRate(), lastTp));
		

		validateLapSplineData(lap, lapTrackpointStore, pauseResumeTimes, lapEndDuration);
		lapTrackpointStore.remove(0);		// Remove the final trackpoint from the previous lap which was added for the purpose of pause/resumes that span across laps.

		long difference = lapTrackpointDurationVsLapEndDuration(lapTrackpointStore, lapEndDuration);
		log.out(Level.FINE, "Lap duration difference after validation:\t%d", difference);

		return lapTrackpointStore;
	}


	private void validateLapSplineData(Node lap, ArrayList<Trackpoint> lapTrackpointStore, ArrayList<Long> pauseResumeDurations,  long lapEndDuration) {

		// If we only have one trackpoint (the final trackpoint from the previous lap) then we don't need to validate.
		if (lapTrackpointStore.size() <= 2) return;

		Node lapTotalTimeSeconds = Util.getFirstChildByNodeName(lap, "TotalTimeSeconds");
		if (lapTotalTimeSeconds == null) return;

		long difference = lapTrackpointDurationVsLapEndDuration(lapTrackpointStore, lapEndDuration);
		log.out(Level.FINE, "Lap duration difference before validation:\t%d", difference);

		// Ensure all trackpoints have a valid distance.
		Trackpoint previousTp = null;
		Iterator<Trackpoint> tpsIt = lapTrackpointStore.iterator();
		while (tpsIt.hasNext()) {
			Trackpoint tp = tpsIt.next();

			// If we don't have a distance value just set it to the previous trackpoint's distance.
			if (tp.getDistance() == null)
				tp.setDistance((previousTp != null) ? previousTp.getDistance() : 0);

			// Record the previous trackpoint.
			tp.setPreviousTrackpoint(previousTp);
			previousTp = tp;
		}

		// As we haven't attempted to remove trackpoints with no distance or create pause/resumes, it is likely we will
		// still have trackpoints whose durations exceed the lap time.
		// I think the primary reason for this (in additiont to normal pause/resumes) is that garmin automatically pauses
		// the device if it is unable to get a satellite waypoint after a certain amount of seconds (seems to be
		// 8 seconds on the 405 with 2.50 firmware).  To counter this I will basically strip away the slowest-paced
		// (hopefully paused) sections of the lap until our final trackpoint has a less than or equal to that of the lap duration.

		// We allow up to 999 millis as due to rounding errors the lap-duration can be up to 1 second out.
		if (difference > 0) {
			log.out("Lap duration still invalid by %d millis, manually stripping.", difference);
			boolean modified = true;

			do {
				modified = false;
				double paceWorst = Double.MAX_VALUE;
				Trackpoint paceWorstTp = null;
				tpsIt = lapTrackpointStore.iterator();
				while (tpsIt.hasNext()) {
					Trackpoint tp = tpsIt.next();
					// If our previous trackpoint is null then this is the first trackpoint so continue to the next trackpoint.
					previousTp = tp.getPreviousTrackpoint();
					if (previousTp == null) continue;

					long duration = tp.getDuration();
					long durationPrevious = tp.getPreviousDuration();
					double distance = tp.getDistance();
					double distancePrevious = tp.getPreviousDistance();

					// Calculate distance & duration differences.
					double distanceIncrease = distance - distancePrevious;
					long durationIncrease = duration - tp.getPreviousDuration();

					// if we have a distance increase of zero then remove the trackpoint and decrement all future trackpoints/pause-resumes by the duration we didn't increase for.
					if ((distanceIncrease == 0) && (durationIncrease > 0)) {
						long decrementLength = (difference > durationIncrease) ? durationIncrease : difference;
						log.out(Level.FINE, "Zero-distance decrement:\tDuration %d\tDistance %.4f\tLength: %d", durationPrevious, distancePrevious, decrementLength);
						tpsIt.remove();
						if (tpsIt.hasNext()) {
							Trackpoint nextTp = tpsIt.next();
							nextTp.decrementDuration(decrementLength);
							nextTp.setPreviousTrackpoint(previousTp);
						}
						while (tpsIt.hasNext()) tpsIt.next().decrementDuration(decrementLength);
						decrementPauseResumes(pauseResumeDurations, durationPrevious, decrementLength);
						modified = true;
						break;
					}

					double pace = (durationIncrease > MILLIS_POTENTIAL_PAUSE_THRESHOLD) ? (distanceIncrease / durationIncrease) : Double.MAX_VALUE;
					if (pace < paceWorst) {
						paceWorst = pace;
						paceWorstTp = tp;
					}
				}

				// If we haven't found any zero-distance trackpiont increases then decrement all future trackpoints/pause-resumes from the slowest pace trackpoint pair of the lap.
				if ((paceWorstTp != null) && !(modified)) {
					log.out(Level.FINEST, "Slowest-pace decrement:\tDuration %d\tDistance %.4f", paceWorstTp.getPreviousDuration(), paceWorstTp.getPreviousDistance());
					modified = true;
					tpsIt = lapTrackpointStore.iterator();
					while (tpsIt.hasNext()) {
						Trackpoint tp = tpsIt.next();
						if (tp.equals(paceWorstTp)) {
							tp.decrementDuration(1000);
							while (tpsIt.hasNext()) tpsIt.next().decrementDuration(1000);
							decrementPauseResumes(pauseResumeDurations, tp.getDuration(), 1000);

						}
					}
				}
			}
			while ((modified) && ((difference = lapTrackpointDurationVsLapEndDuration(lapTrackpointStore, lapEndDuration)) > 0));
		}
		
	}

	/**
	 * Get the difference (in millis) between the duration of the final trackpoint in the trackpoint-store and the expected lap-end-duration.
	 * @param lapTrackpointStore An ArrayList of trackpoints for the lap - the last element in this collection should have duration less than lapEndDuration.
	 * @param lapEndDuration The expected duration at the end of the lap.
	 * @return The difference (positive if the trackpoint store exceeds the expected lap-end-duration).
	 */
	private long lapTrackpointDurationVsLapEndDuration(ArrayList<Trackpoint> lapTrackpointStore, long lapEndDuration) {
		return (lapTrackpointStore.size() == 0)
			? 0
			: roundToNearestThousand(getLastTrackpointFromArrayList(lapTrackpointStore).getDuration()) - roundToNearestThousand(lapEndDuration)
		;
	}

	/**
	 * Use this to round lap durations.  The duration specified in the <code>&lt;Lap&gt;</code> element is to the nearest
	 * millisecond whereas the data in the <code>&lt;Trackpoint&gt;</code> elements is to the nearest second.
	 * @param n The number to round to the nearest thousand.
	 * @return The rounded number.
	 */
	private long roundToNearestThousand(long n) {
		return (Math.round((double) n / 1000) * 1000);
	}


	/**
	 * Decrement pause-resumes which are greater than the durationMinimum.
	 * @param al The ArrayList of pause-resumes to modify.
	 * @param durationMinimum We only modifiy entries with a value greater than this.
	 * @param decrementLength How much to decrement the entries.
	 */
	private void decrementPauseResumes(ArrayList<Long> al, long durationMinimum, long decrementLength) {
		int index = 0;
		for (Long prDuration : al) {
			if (prDuration > durationMinimum)
				al.set(index, prDuration - decrementLength);
			index++;
		}
	}
	

	/**
	 * Call this when we find a 'resume' trackpoint the next trackpoint immeadiately following a 'pause' trackpoint.
	 * We add the pause/resume details to the pauseResumeTImes ArrayList and return the length of time (in millis) that the
	 * device was paused for.
	 * <p>
	 * Nike+ currently just use the same duration/distance to represent both the pause and the resume so I only store the pauseTime.
	 * @param pauseResumeTimes The store of pause/resume times.  If not provided then we don't store the times.
	 * @param resumeTp The 'resume' trackpoint.
	 * @param durationPaused The duration of the previous 'pause' trackpoint.
	 * @return The length of time the device was paused for (so we can update decrement future trackpoint durations).
	 */
	/*
	private long addPauseResume(ArrayList<Long> pauseResumeTimes, Trackpoint resumeTp, long durationPaused) {
		long durationResume = resumeTp.getDuration();
		long pauseLength = (durationResume- durationPaused);
		if (pauseResumeTimes != null) {
			pauseResumeTimes.add(durationPaused);
			log.out(Level.FINER, "Adding pause/resume.\tDuration %d\tDistance %.4f.\tlength %d", durationPaused, resumeTp.getDistance(), pauseLength);
		}
		return pauseLength;
	}
	*/
	

	/**
	 * Generates the following PolynomialSplineFunctions: distanceToDuration, durationToDistance, durationToPace, durationToHeartRate.
	 * @param trackpoints The Trackpoint data from which to build the PolynomialSplineFunctions.
	 * @return A PolynomialSplineFunction array in represnting the data listed in the description above.
	 */
	private PolynomialSplineFunction[] generateSplineFunctions(ArrayList<Trackpoint> trackpoints) throws ConverterException {
		int tpsSize = trackpoints.size();
		double[] durationsArray = new double[tpsSize];
		double[] distancesArray = new double[tpsSize];
		double[] pacesArray = new double[tpsSize];
		double[] heartRatesArray = new double[tpsSize];

		populateDurationsAndDistancesArrays(trackpoints, durationsArray, distancesArray, pacesArray, heartRatesArray);
		
		// Generate cubic splines for distance -> duration, duration -> distance & distance -> pace.
		SplineInterpolator interpolator = new SplineInterpolator();


		if (distancesArray.length > 2 && durationsArray.length > 2) {
			// Generate and return PolynomialSplineFunctions for:
			//  1) distance -> duration
			//  2) duration -> distance
			//  3) duration -> pace
			//  4) duration -> heart-rate
			return new PolynomialSplineFunction[] {
				interpolator.interpolate(distancesArray, durationsArray),
				interpolator.interpolate(durationsArray, distancesArray),
				interpolator.interpolate(durationsArray, pacesArray),
				interpolator.interpolate(durationsArray, heartRatesArray)
			};
		} else {
			throw new ConverterException("Unable to extract GPS data which is required for conversion.");
		}

	}


	private void appendSnapShotListAndExtendedData(Element sportsDataElement, ArrayList<Double> onDemandVPDistances, ArrayList<Long> pauseResumeTimes, 
			PolynomialSplineFunction distanceToDuration, PolynomialSplineFunction durationToDistance, PolynomialSplineFunction durationToPace, PolynomialSplineFunction durationToHeartRate) {
		Element snapShotKmListElement = Util.appendElement(sportsDataElement, "snapShotList", null, "snapShotType", "kmSplit");
		Element snapShotMileListElement = Util.appendElement(sportsDataElement, "snapShotList", null, "snapShotType", "mileSplit");
		Element snapShotClickListElement = Util.appendElement(sportsDataElement, "snapShotList", null, "snapShotType", "userClick");

		// Create a double array representing the on-demand-vp clicks - ignoring the final one as this is covered by the "stop" event.
		int odvpsCount = onDemandVPDistances.size() - 1;
		double[] odvps = new double[(odvpsCount >= 0) ? odvpsCount : 0];
		Iterator<Double> odvpsIt = onDemandVPDistances.iterator();
		int index = 0;
		while (odvpsIt.hasNext() && (index < odvpsCount)) odvps[index++] = odvpsIt.next();

		// Pause/Resume splits.
		int odvpsIndex = 0;
		for (long pauseDuration : pauseResumeTimes) {
			double distance = interpolate(durationToDistance, pauseDuration);

			// Add all onDemandVP clicks leading up to this pause/resume.
			odvpsIndex = addOnDemandVPClicks(snapShotClickListElement, odvps, odvpsIndex, distance, distanceToDuration, durationToPace, durationToHeartRate);
			
			// Sometimes a pause/resume happens at the very end of a workout - if this is the case then we will not have decremented the
			// durations properly so just leave them out - there's no point in documeting a pause directly before the end of the workout anyway.
			if (pauseDuration < _totalDuration) {
				long pace = (long) interpolate(durationToPace, pauseDuration);
				int heartRateBpm = (int) interpolate(durationToHeartRate, pauseDuration);

				// 2009-12-01: Looking at various runs on nike+ it seems the each pause/resume pair now has the same duration/distance
				// (using the pause event).  I can't find my nike+ stuff to check this is 100% accurate but will go with it for now.
				appendSnapShot(snapShotClickListElement, pauseDuration, distance, pace, heartRateBpm, "event", "pause");
				appendSnapShot(snapShotClickListElement, pauseDuration, distance, pace, heartRateBpm, "event", "resume");
			}
		}

		// Add all remaining onDemandVP clicks leading up to the stop click.
		addOnDemandVPClicks(snapShotClickListElement, odvps, odvpsIndex, _totalDistance, distanceToDuration, durationToPace, durationToHeartRate);

		// Km splits
		for (int i = 1000; i <= _totalDistance; i += 1000) {
			double duration = (long) interpolate(distanceToDuration, i);
			appendSnapShot(snapShotKmListElement, (long) duration, i, (long) interpolate(durationToPace, duration), (int) interpolate(durationToHeartRate, duration));
		}

		// Mile splits
		for (double i = D_METRES_PER_MILE; i <= _totalDistance; i += D_METRES_PER_MILE) {
			double duration = (long) interpolate(distanceToDuration, i);
			appendSnapShot(snapShotMileListElement, (long) duration, i, (long) interpolate(durationToPace, duration), (int) interpolate(durationToHeartRate, duration));
		}

		// Stop split
		appendSnapShot(snapShotClickListElement, _totalDuration, _totalDistance, (long) interpolate(durationToPace, _totalDuration), (int) interpolate(durationToHeartRate, _totalDuration), "event", "stop");

		// ExtendedDataLists
		appendExtendedDataList(sportsDataElement, durationToDistance, durationToPace, durationToHeartRate);
	}


	/**
	 * Adds onDemandVP elements from the odvps array to the snapShotClickList only when their index in the array >= odvpsIndex and their value < distanceLimit.
	 * <br /> It incrememnts the odvpsIndex amount for each onDemandVP element it adds.
	 * <br />Once it has completed (0...n iterations) it returns the updated odvpsIndex value.
	 * @param snapShotClickListElement The element to add the onDemandVP elements to.
	 * @param odvps An array of distances representing all the onDemandVP elements we need to create for this workout.
	 * @param odvpsIndex We ignore all cells in the odvps array before this index (it's likely onDeamndVP elements for them have already been created).
	 * @param distanceLimit We stop iterating when we find an odvps cell >= this amount.
	 * @param distanceToDuration PolynomialSplineFunction for converting distances to duration.
	 * @param durationToPace PolynomialSplineFunction for converting duraiton to pace.
	 * @param durationToHeartRate PolynomialSplineFunction for converting duration to heart rate.
	 * @return Updated odvpsIndex value.
	 */
	private int addOnDemandVPClicks(Element snapShotClickListElement, double[] odvps, int odvpsIndex, double distanceLimit, 
			PolynomialSplineFunction distanceToDuration, PolynomialSplineFunction durationToPace, PolynomialSplineFunction durationToHeartRate) {
		while ((odvpsIndex < odvps.length) && (odvps[odvpsIndex] <= distanceLimit)) {
			log.out(Level.FINE, "onDemandVP %d\tdistance-since-previous: %f", odvpsIndex + 1, (odvpsIndex == 0) ? odvps[odvpsIndex] : odvps[odvpsIndex] - odvps[odvpsIndex-1]);
			double odvpDistance = odvps[odvpsIndex++];
			long odvpDuration = (long) interpolate(distanceToDuration, odvpDistance);
			long odvpPace = (long) interpolate(durationToPace, odvpDuration);
			int odvpHeartRateBpm = (int) interpolate(durationToHeartRate, odvpDuration);
			appendSnapShot(snapShotClickListElement, odvpDuration, odvpDistance, odvpPace, odvpHeartRateBpm, "event", "onDemandVP");
		}

		return odvpsIndex;
	}


	// Copy data from the trackpoint ArrayList into the durations & distances arrays in preparation for creating PolynomialSplineFunction.
	private void populateDurationsAndDistancesArrays(ArrayList<Trackpoint> trackpoints, double[] durations, double[] distances, double[] paces, double[] heartRates) {
		int i = 0;
		for (Trackpoint tp : trackpoints) {
			if (_includeHeartRateData) heartRates[i] = tp.getHeartRate();
			durations[i] = tp.getDuration();
			distances[i] = tp.getDistance();
			paces[i++] = tp.getPace();
		}
	}
	

	/*
	<extendedDataList>
	  <extendedData dataType="distance" intervalType="time" intervalUnit="s" intervalValue="10">0.0. 1.1, 2.3, 4.7, etc</extendedData>
	</extendedDataList>
	*/
	private void appendExtendedDataList(Element sportsDataElement, PolynomialSplineFunction durationToDistance, PolynomialSplineFunction durationToPace, PolynomialSplineFunction durationToHeartRate) {
		final int finalReading = (int) durationToDistance.getKnots()[durationToDistance.getN() - 1];
		double previousDistance = 0;
		StringBuilder sbDistance = new StringBuilder("0.0");
		StringBuilder sbSpeed = new StringBuilder("0.0");
		StringBuilder sbHeartRate = new StringBuilder();

		if (_includeHeartRateData) sbHeartRate.append((int) interpolate(durationToHeartRate, 0d));

		for (int i = 10000; i < finalReading; i = i + 10000) {

			// Distance
			double distance = (interpolate(durationToDistance, i))/1000;

			// 2010-06-18: Phillip Purcell emailed me two workouts on 2010-06-14 which did not draw graphs on nikeplus.com
			// A bit of debugging showed that some of the interpolated distances in these workouts were  less than the
			// previous distance (impossible).  Hacked this fix so that this will never happen.
			if (distance < previousDistance) distance = previousDistance;
			sbDistance.append(String.format(", %.4f", distance));
			previousDistance = distance;

			// Speed
			sbSpeed.append(String.format(", %.4f", MILLIS_PER_HOUR/(interpolate(durationToPace, i))));

			// Heart Rate
			if (_includeHeartRateData) sbHeartRate.append(String.format(", %d", (int) interpolate(durationToHeartRate, i)));
		}

		Element extendedDataListElement	= Util.appendElement(sportsDataElement, "extendedDataList");
		Util.appendElement(extendedDataListElement, "extendedData", sbDistance, "dataType", "distance", "intervalType", "time", "intervalUnit", "s", "intervalValue", "10");
		Util.appendElement(extendedDataListElement, "extendedData", sbSpeed, "dataType", "speed", "intervalType", "time", "intervalUnit", "s", "intervalValue", "10");

		if (_includeHeartRateData)
			Util.appendElement(extendedDataListElement, "extendedData", sbHeartRate, "dataType", "heartRate", "intervalType", "time", "intervalUnit", "s", "intervalValue", "10");

	}


	private void appendSnapShot(Element snapShotListElement, long durationMillis, double distanceMetres, long paceMillisKm, int heartRateBpm, String ... attributes) {
		Element snapShotElement = Util.appendElement(snapShotListElement, "snapShot", null, attributes);

		Util.appendElement(snapShotElement, "duration", durationMillis);
		Util.appendElement(snapShotElement, "distance", String.format("%.3f", distanceMetres/1000));
		Util.appendElement(snapShotElement, "pace", paceMillisKm);
		if (_includeHeartRateData)
			Util.appendElement(snapShotElement, "bpm", heartRateBpm);
	}


	private double interpolate(PolynomialSplineFunction spline, double x) {
		// Ensure that spline-min <= x <= spline-max
		double[] knots = spline.getKnots();
		x = Math.max(x, knots[0]);
		x = Math.min(x, knots[knots.length - 1]);
		return spline.value(x);
	}
	


	public String generateFileName() {
		SimpleDateFormat outfileFormat = new SimpleDateFormat("yyyy-MM-dd HH;mm;ss'.xml'");
		outfileFormat.setTimeZone(_workoutTimeZone);
		return outfileFormat.format(_calEnd.getTime());
	}

	public String getStartTimeHumanReadable() {
		return _startTimeStringHuman;
	}


	private Trackpoint getLastTrackpointFromArrayList(ArrayList<Trackpoint> al) {
		return getTrackpointFromEndofArrayList(al, 0);
	}

	private Trackpoint getTrackpointFromEndofArrayList(ArrayList<Trackpoint> al, int placesFromEnd) {
		return ((al != null) && (al.size() > placesFromEnd)) ? al.get(al.size() - (placesFromEnd + 1)) : null;
	}

	public long getTotalDurationMillis() {
		return _totalDuration;
	}
	public double getTotalDistanceMetres() {
		return _totalDistance;
	}




	public static void main(String[] args) {
		File inFile = new File(args[0]);

		String empedID = (args.length >= 2) ? args[1] : null;

		ConvertTcx c = new ConvertTcx();
		try {
			Document doc = c.generateNikePlusXml(inFile, empedID);
			Util.writeDocument(doc, c.generateFileName());
		}
		catch (Throwable t) {
			log.out(t);
		}
	}


	


	private class Trackpoint
	{
		private static final int		PACE_MILLIS = 20 * 10000;		// How many milli seconds of data to use when calculating pace.

		private Long		_duration;
		private Double		_distance;
		private Double		_heartRate;
		private Trackpoint	_previousTrackpoint;

		public Trackpoint(Trackpoint previousTrackpoint) {
			_previousTrackpoint = previousTrackpoint;
		}

		/**
		 * A representatino of a garmin <trackpoint> element.
		 * @param duration Current (relative to start of workout) duration in millis.
		 * @param distance Current distance in metres.
		 * @param heartRate Current heartrate in bpm.
		 * @param previousTrackpoint The previously recorded trackpoint, or null if this is the first trackpoint.
		 */
		public Trackpoint(long duration, double distance, Double heartRate, Trackpoint previousTrackpoint) {
			_duration = duration;
			_distance = distance;
			_heartRate = heartRate;
			_previousTrackpoint = previousTrackpoint;
		}


		protected Long getDuration() {
			return _duration;
		}

		protected Double getDistance() {
			return _distance;
		}

		protected Double getHeartRate() {
			return _heartRate;
		}

		protected void setDuration(Long duration) {
			_duration = duration;
		}

		protected void setDistance(Double distance) {
			_distance = distance;
		}

		protected void setHeartRate(Double heartRate) {
			_heartRate = heartRate;
		}

		protected void setPreviousTrackpoint(Trackpoint previousTrackpoint) {
			_previousTrackpoint = previousTrackpoint;
		}


		/**
		 * Nike+ uses the number of millis it takes to complete 1km as their pace figure.
		 * @return Nike+ pace.
		 */
		protected long getPace() {
			// This can give skewed pace figures as we might have a trackpoint 2 seconds ago with slightly inaccurate distance resulting in wildly chaotic pace data.
			//Trackpoint previous = getPreviousDifferentTrackpoint();

			// I use PACE_MILLIS previous seconds of data to calculate pace (avoiding the problem caused by getPreviousDifferntTrackpoint).
			long startDuration = (_duration > PACE_MILLIS) ? _duration - PACE_MILLIS : 0;
			Trackpoint startTp = getPreviousTrackpoint();
			while ((startTp != null) && (startTp.getDuration() > startDuration))
				startTp = startTp.getPreviousTrackpoint();

			long splitDuration = (startTp == null) ? _duration : (_duration - startTp.getDuration());
			double splitDistance = (startTp == null) ? _distance : (_distance - startTp.getDistance());

			return (long) ((1000d/splitDistance) * splitDuration);
		}

		protected Trackpoint getPreviousTrackpoint() {
			return _previousTrackpoint;
		}

		protected Double getMostRecentDistance() {
			Trackpoint tp = this;
			while ((tp != null)) {
				if (tp.getDistance() != null) return tp.getDistance();
				tp = tp.getPreviousTrackpoint();
			}
			return 0d;
		}

		protected long getDurationSinceLastTrackpoint() {
			return (_previousTrackpoint == null) ? 0 : (_duration - _previousTrackpoint.getDuration());
		}

		protected boolean isRepeatDuration() {
			//return (_previousTrackpoint != null) && (_duration.equals(_previousTrackpoint.getDuration()));
			return (_previousTrackpoint != null) && ((_duration.compareTo(_previousTrackpoint.getDuration())) <= 0);
		}

		protected boolean isRepeatDistance() {
			//return (_previousTrackpoint != null) && (_distance.equals(_previousTrackpoint.getDistance()));
			//return (_previousTrackpoint != null) && ((_distance.compareTo(_previousTrackpoint.getDistance())) <= 0);
			return (_previousTrackpoint != null) && (_distance - _previousTrackpoint.getDistance() <= 1);	// Consider this a repeat trackpoint if the difference is less than one metre.
		}

		protected Long getPreviousDuration() {
			return (_previousTrackpoint == null) ? 0 : _previousTrackpoint.getDuration();
		}

		protected Double getPreviousDistance() {
			return (_previousTrackpoint == null) ? 0 : _previousTrackpoint.getDistance();
		}

		/*
		protected void incrementDuration(long millis) {
			_duration += millis;
		}
		*/
		
		protected void decrementDuration(long millis) {
			_duration -= millis;
		}

		
		@Override
		public String toString() {
			return String.format("Duration (difference): %d\t(%d)\t\tDistance: %.4f", _duration, (_duration - ((_previousTrackpoint == null) ? 0 : _previousTrackpoint.getDuration())), _distance);
		}
	}
} public class ConverterException extends Exception {
	public ConverterException(@Nonnull String message) {
		super(Preconditions.checkNotNull(message, "message argument is null."));
	}

	public ConverterException(String message, Throwable throwable) {
		super(Preconditions.checkNotNull(message, "message argument is null."), Preconditions.checkNotNull(throwable, "throwable argument is null."));
	}
} public class GpxToRunJson implements Converter<GpxType, RunJson> {

	private static final @Nonnull Log logger = Log.getInstance();

	private static final @Nonnull List<RunJson.Summary.DeviceConfig> DEFAULT_DEVICE_CONFIGS = ImmutableList.of(new RunJson.Summary.DeviceConfig(new RunJson.Summary.DeviceConfig.Component("iphone", "device")));
	private static final @Nonnull String METRIC_DATA_POINT = "dataPoint";

	/*
	Required output for json:
	    distance - km
	    duration - ms
	    starttime - ms since epoch.
	    timeZoneId - Europe/London
	    type - run
	 */
	@Override
	public RunJson convert(@Nonnull GpxType gpxDocument) throws ConverterException {
		Preconditions.checkNotNull(gpxDocument, "gpxDocument argument is null.");

		logger.out("Converting GpxType to RunJson");
		SplineFunctions splineFunctions = generateSplines(gpxDocument);

		ImmutableList.Builder<RunJson.Detail> runJsonDetailBuilder = ImmutableList.<RunJson.Detail>builder().add(
				generateDetail("distance", "time", "sec", 0L, 10L, "dataStream", splineFunctions.durationToDistance, 3)
		);
		if (splineFunctions.durationToHeartRate != null) {
			runJsonDetailBuilder.add(
					generateDetail("heartrate", "time", "sec", 0L, 10L, "dataStream", splineFunctions.durationToHeartRate, 0)
			);
		}

		RunJson.Summary runJsonSummary = new RunJson.Summary(
				ImmutableList.of(
						generateSnaphot("mileSplit", splineFunctions.distanceToDuration, DistanceUtils.MILES_TO_KM),
						generateSnaphot("kmSplit", splineFunctions.distanceToDuration, 1)
				),
				DEFAULT_DEVICE_CONFIGS
		);

		BigDecimal maxDistance = new BigDecimal(getMaxX(splineFunctions.distanceToDuration)).setScale(6, RoundingMode.HALF_EVEN);
		BigDecimal maxDuration = new BigDecimal(getMaxX(splineFunctions.durationToDistance)).setScale(6, RoundingMode.HALF_EVEN);

		WptType firstTrkpt = getFirstTrkpt(gpxDocument);
		try {
			TimeZone timezone = Geonames.getTimeZone(firstTrkpt.getLon(), firstTrkpt.getLat());
			long startTime = firstTrkpt.getTime().toGregorianCalendar().getTimeInMillis();
			return new RunJson(maxDistance, maxDuration, startTime, "complete", timezone.getID(), "run", runJsonDetailBuilder.build(), runJsonSummary);
		} catch (Throwable throwable) {
			throw new ConverterException(throwable.getMessage(), throwable);
		}
	}

	private double getMaxX(@Nonnull PolynomialSplineFunction splineFunction) {
		double[] knots = splineFunction.getKnots();
		return knots[knots.length - 1];
	}

	private @Nonnull RunJson.Detail generateDetail(@Nonnull String metricType,
												   @Nonnull String intervalType,
												   @Nonnull String intervalUnit,
												   long startTimeOffset,
												   long intervalMetric,
												   @Nonnull String objType,
												   @Nonnull PolynomialSplineFunction splineFunction,
												   int roundingScale) {
		logger.out(" - generating detail for %s", metricType);
		double[] knots = splineFunction.getKnots();
		double maxValue = knots[knots.length - 1];

		// IntervalMetric is seconds, we have our data stored as milliseconds, so multiply by 1000.
		long loopIncrement = intervalMetric * 1000;

		List<BigDecimal> values = new ArrayList<>();
		for (long i = loopIncrement; i <= maxValue; i += loopIncrement) {
			BigDecimal value = new BigDecimal(splineFunction.value(i));
			values.add(value.setScale(roundingScale, RoundingMode.HALF_EVEN));
		}

		return new RunJson.Detail(metricType, intervalType, intervalUnit, startTimeOffset, String.valueOf(intervalMetric), objType, values);
	}

	private @Nonnull RunJson.Summary.Snapshot generateSnaphot(@Nonnull String name, @Nonnull PolynomialSplineFunction splineFunction, double metricInterval) {
		logger.out(" - generating snapshot for %s", name);
		double[] knots = splineFunction.getKnots();
		int dataSeriesLength = (int) (knots[knots.length - 1] / metricInterval);

		List<RunJson.Summary.Snapshot.DataSeries> dataSeries = new ArrayList<>(dataSeriesLength);

		for (int i = 1; i <= dataSeriesLength; ++i) {
			long value = (long) splineFunction.value(i * metricInterval);
			dataSeries.add(createDataSeries(i, value, METRIC_DATA_POINT));
		}

		return new RunJson.Summary.Snapshot(name, dataSeries);
	}

	private @Nonnull RunJson.Summary.Snapshot.DataSeries createDataSeries(int distance, long duration, @Nonnull String objType) {
		RunJson.Summary.Snapshot.DataSeries.Metrics metric = new RunJson.Summary.Snapshot.DataSeries.Metrics(distance, duration);
		return new RunJson.Summary.Snapshot.DataSeries(metric, objType);
	}

	private @Nonnull WptType getFirstTrkpt(@Nonnull GpxType gpxDocument) {
		for (TrkType trks : gpxDocument.getTrk()) {
			for (TrksegType trkSeg : trks.getTrkseg()) {
				for (WptType trkpt : trkSeg.getTrkpt()) {
					if (isValidTrkpt(trkpt)) {
						return trkpt;
					}
				}
			}
		}

		throw new IllegalStateException("GPX document doesn't have any <trkpt/> elements, which are required.");
	}


	private static boolean isValidTrkpt(@Nonnull WptType trkpt) {
		return trkpt.getLon() != null && trkpt.getLat() != null && trkpt.getTime() != null;
	}

	private static final Predicate<WptType> IS_VALID_TRKPT = new Predicate<WptType>() {
		@Override
		public boolean apply(@Nullable WptType trkpt) {
			return trkpt != null && isValidTrkpt(trkpt);
		}
	};

	@Nonnull SplineFunctions generateSplines(@Nonnull GpxType gpxDocument) throws ConverterException {
		Preconditions.checkNotNull(gpxDocument, "gpxDocument argument is null.");
		logger.out(" - generating splines");

		List<Long> durations = Lists.newArrayList(0L);
		List<Double> distances = Lists.newArrayList(0d);
		List<Short> heartRates = new ArrayList<>();

		DistanceCalculator distanceCalculator = new GeodesicSphereDistCalc.Vincenty();

		long totalPausedTime = 0;
		double totalDistance = 0;
		long workoutStartTime = 0;
		long previousDuration = 0;
		Point previousPoint = null;


		for (TrkType trks : gpxDocument.getTrk()) {

			for (TrksegType trkSeg : trks.getTrkseg()) {
				// Each <trkseg /> element  represents a period where the device is not paused.
				// We need at least 2 <trkpt /> elemnts to calculate anything useful.
				if (trkSeg.getTrkpt().size() > 1) {

					Iterator<WptType> trkptIt = Iterators.filter(trkSeg.getTrkpt().iterator(), IS_VALID_TRKPT);

					// Get the first <trkpt /> in this <trkSeg />
					WptType trkpt = trkptIt.next();

					// If this is our very first <trkpt /> in our workout...
					if (previousPoint == null) {
						workoutStartTime = trkpt.getTime().toGregorianCalendar().getTimeInMillis();
						previousPoint = getPoint(trkpt);

						// If we have heart rate data, add it
						Short heartRate = getHeartRate(trkpt);
						if (heartRate != null) {
							heartRates.add(heartRate);
						}
					} else {
						long duration = getMillisSinceWorkoutStart(trkpt.getTime(), workoutStartTime, totalPausedTime);
						long pausedTime = duration - previousDuration;
						totalPausedTime += pausedTime;
						previousPoint = getPoint(trkpt);
					}

					// Iterate through the remaining <trkpt />'s in this <trkseg />, adding the duration/distance for each.
					while (trkptIt.hasNext()) {
						trkpt = trkptIt.next();

						long duration = getMillisSinceWorkoutStart(trkpt.getTime(), workoutStartTime, totalPausedTime);

						if (duration > previousDuration) {
							Point point = getPoint(trkpt);
							if (!point.equals(previousPoint)) {
								durations.add(duration);

								double distanceKm = distanceCalculator.distance(previousPoint, point) * DistanceUtils.DEG_TO_KM;
								totalDistance += distanceKm;
								distances.add(totalDistance);

								// If we have heart rate data, add it
								Short heartRate = getHeartRate(trkpt);
								if (heartRate != null) {
									heartRates.add(heartRate);
								}

								previousDuration = duration;
								previousPoint = point;
							}
						}
					}
				}
			}
		}

		// If we less than 3 points we can't interpolate
		if (durations.size() > 2) {
			SplineInterpolator interpolator = new SplineInterpolator();
			PolynomialSplineFunction durationToDistanceFunction = interpolator.interpolate(Doubles.toArray(durations), Doubles.toArray(distances));
			PolynomialSplineFunction distanceToDurationFunction = interpolator.interpolate(Doubles.toArray(distances), Doubles.toArray(durations));
			PolynomialSplineFunction durationToHeartRateFunction = (heartRates.size() == durations.size())
					? interpolator.interpolate(Doubles.toArray(durations), Doubles.toArray(heartRates))
					: null;

			return new SplineFunctions(durationToDistanceFunction, distanceToDurationFunction, durationToHeartRateFunction);
		} else {
			throw new ConverterException("Unable to extract GPS data which is required for conversion.");
		}
	}


	private @Nullable Short getHeartRate(@Nonnull WptType trkpt) {
		ExtensionsType extensions = trkpt.getExtensions();
		if (extensions != null) {
			for (Object xsdAny : extensions.getAny()) {
				if (xsdAny instanceof JAXBElement<?>) {
					Object xsdAnyValue = ((JAXBElement) xsdAny).getValue();
					if (xsdAnyValue instanceof TrackPointExtensionT) {
						TrackPointExtensionT trackPointExtension = (TrackPointExtensionT) xsdAnyValue;
						if (trackPointExtension.getHr() != null) {
							return trackPointExtension.getHr();
						}
					}
				}
			}
		}
		return null;
	}


	private @Nonnull Point getPoint(@Nonnull WptType trkpt) {
		return new PointImpl(trkpt.getLon().doubleValue(), trkpt.getLat().doubleValue(), SpatialContext.GEO);
	}

	private long getMillisSinceWorkoutStart(@Nonnull XMLGregorianCalendar currentTime, long workoutStartTime, long totalTimePaused) {
		return currentTime.toGregorianCalendar().getTimeInMillis() - workoutStartTime - totalTimePaused;
	}


	private static class SplineFunctions {
		private final @Nonnull PolynomialSplineFunction durationToDistance;
		private final @Nonnull PolynomialSplineFunction distanceToDuration;
		private final @Nullable PolynomialSplineFunction durationToHeartRate;

		private SplineFunctions(@Nonnull PolynomialSplineFunction durationToDistance, @Nonnull PolynomialSplineFunction distanceToDuration, @Nullable PolynomialSplineFunction durationToHeartRate) {
			this.durationToDistance = Preconditions.checkNotNull(durationToDistance, "durationToDistance argument is null.");
			this.distanceToDuration = Preconditions.checkNotNull(distanceToDuration, "distanceToDuration argument is null.");
			this.durationToHeartRate = durationToHeartRate;
		}
	}
} @Deprecated
public class GarminActivityData {
	private final @Nonnull Document tcxDocument;
	private final @Nullable Document gpxDocument;

	public GarminActivityData(@Nonnull Document tcxDocument, @Nullable Document gpxDocument) {
		this.tcxDocument = Preconditions.checkNotNull(tcxDocument, "tcxDocument argument is null.");
		this.gpxDocument = gpxDocument;
	}

	public @Nonnull Document getTcxDocument() {
		return tcxDocument;
	}

	public @Nullable Document getGpxDocument() {
		return gpxDocument;
	}
} public enum GarminDataType {
	TCX {
		private static final @Nonnull String DOWNLOAD_URL = "https://connect.garmin.com/proxy/activity-service-1.0/tcx/activity/%d?full=true";

		@Override
		@Nonnull String getDownloadUrlStringFormat() {
			return DOWNLOAD_URL;
		}

		@Override
		@Nonnull JAXBObject getJAXBObject() {
			return JAXBObject.TRAINING_CENTER_DATABASE;
		}
	},
	GPX {
		private static final @Nonnull String DOWNLOAD_URL = "https://connect.garmin.com/proxy/activity-service-1.1/gpx/activity/%s?full=true";

		@Override
		@Nonnull String getDownloadUrlStringFormat() {
			return DOWNLOAD_URL;
		}

		@Override
		@Nonnull JAXBObject getJAXBObject() {
			return JAXBObject.GPX_TYPE;
		}
	};

	private static final @Nonnull Log logger = Log.getInstance();
	private static final @Nonnull String URL_ACTIVITIES = "https://connect.garmin.com/activities";
	private static final @Nonnull String URL_SIGN_IN = "http://connect.garmin.com/en-US/signin";

	private static final int GARMIN_DOWNLOAD_SUCCESS = HttpStatus.SC_OK;
	private static final int GARMIN_DOWNLOAD_FORBIDDEN = HttpStatus.SC_FORBIDDEN;

	// Load "garmin.properties" file.
	private static final @Nonnull Properties garminProperties = new Properties();
	static {
		String propertiesFile = "/garmin.properties";
		logger.out(Level.FINER, "loading %s", propertiesFile);
		try (InputStream inputStream = GarminDataType.class.getResourceAsStream(propertiesFile)) {
			garminProperties.load(inputStream);
		}
		catch (IOException ioe) {
			throw new ExceptionInInitializerError(ioe);
		}
	}

	// Garmin login form entity.
	private static final @Nonnull UrlEncodedFormEntity GARMIN_LOGIN_FORM_ENGITY = createGarminLoginFormEntity();
	private static @Nonnull UrlEncodedFormEntity createGarminLoginFormEntity() {
		try {
			return new UrlEncodedFormEntity(
					ImmutableList.of(
							new BasicNameValuePair("login", "login"),
							new BasicNameValuePair("login:loginUsernameField", garminProperties.getProperty("GARMIN_USERNAME")),
							new BasicNameValuePair("login:password", garminProperties.getProperty("GARMIN_PASSWORD")),
							new BasicNameValuePair("javax.faces.ViewState", "j_id1")
					)
			);
		} catch (UnsupportedEncodingException e) {
			throw new ExceptionInInitializerError(e);
		}
	}

	abstract @Nonnull String getDownloadUrlStringFormat();
	abstract @Nonnull JAXBObject getJAXBObject();


	public static @Nonnull CloseableHttpClient getGarminHttpSession() throws IOException {
		logger.out("Opening garmin HTTP session");

		CloseableHttpClient client = HttpClients.createDefaultHttpClientBuilder().build();

		// 1 - Initial attempt to view activities, which will set cookies and redirect us to sign-in page.
		logger.out(Level.FINER," - getting cookie");
		HttpGet get = new HttpGet(URL_ACTIVITIES);
		try (CloseableHttpResponse response1 = client.execute(get)) {
			EntityUtils.consumeQuietly(response1.getEntity());
			logger.out(Level.FINE, "   - response code: %d", response1.getStatusLine().getStatusCode());

			// 2 - Sign-in attempt
			logger.out(Level.FINE, " - signing in");
			HttpPost post = new HttpPost(URL_SIGN_IN);
			post.setEntity(GARMIN_LOGIN_FORM_ENGITY);
			try (CloseableHttpResponse response2 = client.execute(post)) {
				EntityUtils.consumeQuietly(response2.getEntity());
				logger.out(Level.FINE, "   - response code: %d", response2.getStatusLine().getStatusCode());
			}

			logger.out(" - opened garmin HTTP session");
			return client;
		}
	}

	public @Nonnull CloseableHttpResponse executeGarminHttpRequest(@Nonnull CloseableHttpClient httpClient, int activityId) throws IOException, URISyntaxException {
		Preconditions.checkNotNull(httpClient, "httpClient argument is null.");

		URI uri = generateDownloadUri(activityId);
		logger.out("Executing garmin HTTP request: %s", uri);
		HttpGet get = new HttpGet(uri);
		CloseableHttpResponse closeableHttpResponse = httpClient.execute(get);

		int statusCode = closeableHttpResponse.getStatusLine().getStatusCode();
		logger.out(Level.FINE, " - response code: %d", statusCode);
		if (closeableHttpResponse.getEntity() == null) {
			throw new IOException(String.format("Response entity is null for %s", uri));
		} else if (statusCode == GARMIN_DOWNLOAD_FORBIDDEN) {
			throw new HttpException(String.format("Unable to download data for garmin activity-id %d.<br /></br />Please ensure your garmin activity is not marked as private.", activityId), statusCode);
		} if (statusCode != GARMIN_DOWNLOAD_SUCCESS) {
			throw new HttpException(String.format("Unable to download data for garmin activity-id %d.<br /></br />%s returned HTTP status code %d.", activityId, uri.toString(), statusCode), statusCode);
		}

		return closeableHttpResponse;
	}


	public @Nonnull <T> T downloadAndUnmarshall(@Nonnull CloseableHttpClient httpClient, int activityId) throws JAXBException, IOException, URISyntaxException {
		Preconditions.checkNotNull(httpClient, "httpClient argument is null.");

		try (CloseableHttpResponse response = executeGarminHttpRequest(httpClient, activityId)) {
			try (InputStream inputStream = response.getEntity().getContent()) {
				T output = unmarshall(inputStream);
				EntityUtils.consumeQuietly(response.getEntity());
				return output;
			}
		}
	}

	public @Nonnull <T> T unmarshall(@Nonnull InputStream inputStream) throws JAXBException {
		logger.out("Unmarshalling InputStream to %s", getJAXBObject().name());
		return getJAXBObject().unmarshall(inputStream);
	}

	@Nonnull URI generateDownloadUri(int activityId) throws URISyntaxException {
		return new URI(String.format(getDownloadUrlStringFormat(), activityId));
	}
} @Deprecated
public class Garmin {

	private static final Log log = Log.getInstance();

	private static final String GARMIN_ID_ERROR = "Unable to download garmin activity.  Please ensure your garmin workout is not marked as private.<br /><br />" +
			"If the workout was recorded on a non-garmin device and you have an original GPX file, please upload that using the 'GPX File' checkbox.";


	@Deprecated
	public static Document downloadGarminTcx(CloseableHttpClient client, int garminActivityId) throws IOException {
		return downloadAndCreateDocument(client, garminActivityId, GarminDataType.TCX);
	}

	@Deprecated
	public static Document downloadGarminGpx(CloseableHttpClient httpClient, int garminActivityId) throws IOException {
		return downloadAndCreateDocument(httpClient, garminActivityId, GarminDataType.GPX);
	}

	@Deprecated
	private static Document downloadAndCreateDocument(@Nonnull CloseableHttpClient httpClient, int garminActivityId, @Nonnull GarminDataType garminDataType) throws IOException {
		try (CloseableHttpResponse response = garminDataType.executeGarminHttpRequest(httpClient, garminActivityId)) {
			Document output = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(response.getEntity().getContent());
			EntityUtils.consumeQuietly(response.getEntity());
			return output;
		} catch (HttpException he) {
			throw he;
		} catch (IOException | ParserConfigurationException | SAXException | URISyntaxException e) {
			log.out(e);
			throw new IOException(GARMIN_ID_ERROR);
		}
	}
} public class NikePlus {

	private static final @Nonnull Log logger = Log.getInstance();

	// Load "nikeplus.properties" file.
	private static final @Nonnull Properties nikePlusProperties = new Properties();
	static {
		String propertiesFile = "/nikeplus.properties";
		logger.out(Level.FINER, "loading %s", propertiesFile);
		try (InputStream inputStream = NikePlus.class.getResourceAsStream(propertiesFile)) {
			nikePlusProperties.load(inputStream);
		}
		catch (IOException ioe) {
			throw new ExceptionInInitializerError(ioe);
		}
	}

	private static final @Nonnull String URL_LOGIN_DOMAIN = "secure-nikeplus.nike.com";
	private static final @Nonnull String URL_LOGIN = String.format("https://%s/login/loginViaNike.do?mode=login", URL_LOGIN_DOMAIN);
	private static final @Nonnull String URL_DATA_SYNC = "https://api.nike.com/v2.0/me/sync?access_token=%s";
	private static final @Nonnull String URL_DATA_SYNC_COMPLETE_ACCESS_TOKEN = "https://api.nike.com/v2.0/me/sync/complete";

	private static final @Nonnull String USER_AGENT = "NPConnect";

	public static final @Nonnull String INVALID_PASSWORD_ERROR_MESSAGE = "Nike+'s upload service does not support passwords containing the following characters:" +
			"<pre>\" & ' < ></pre>" +
			"<br />Please change your password on the Nike+ website, apologies this is out of my control";

	private static final int URL_DATA_SYNC_SUCCESS = HttpStatus.SC_OK;

	// From http://support-en-us.nikeplus.com/app/answers/detail/a_id/31352/p/3169,3195
	// If you receive a notification that "Your Email or Password Was Entered Incorrectly", make sure your password
	// does not contain a greater than symbol, ampersand or apostrophe (>, & or `). If your password contains any of
	// these symbols, reset your password.
	// That message seems to be incorrect.  In reality, they are unable to process the "Predefined entities in XML":
	// http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references#Predefined_entities_in_XML
	private static final @Nonnull Pattern INVALID_NIKEPLUS_PASSWORD = Pattern.compile("[\"&'<>]");

	private static final @Nonnull Predicate<char[]> PASSWORD_INVALID = new Predicate<char[]>() {
		@Override
		public boolean apply(@Nullable char[] password) {
			return password == null || INVALID_NIKEPLUS_PASSWORD.matcher(new String(password)).find();
		}
	};

	static @Nonnull UrlEncodedFormEntity generateFormNameValuePairs(@Nonnull String... inputKeyValues) {
		Preconditions.checkNotNull(inputKeyValues, "inputKeyValues argument is null.");
		int inputLength = inputKeyValues.length;
		Preconditions.checkArgument(inputLength > 0, "No input key/values specified.");
		Preconditions.checkArgument((inputLength % 2) == 0, String.format("Odd number of name-value pairs: %d.", inputLength));

		List<NameValuePair> formParams = new ArrayList<>();
		for (int i = 0; i < inputLength;) {
			formParams.add(new BasicNameValuePair(inputKeyValues[i++], inputKeyValues[i++]));
		}

		return new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8);
	}


	private static @Nonnull SetCookie createCookie(@Nonnull String key, @Nonnull String value) {
		SetCookie cookie = new BasicClientCookie(key, value);
		cookie.setPath("/");
		cookie.setDomain(URL_LOGIN_DOMAIN);
		return cookie;
	}

	public static boolean isPasswordValid(@Nonnull char[] nikePassword) {
		Preconditions.checkNotNull(nikePassword, "nikePassword argument is null.");
		return (!PASSWORD_INVALID.apply(nikePassword));
	}

	/**
	 * Performs a login to nike+, returning the nike+ access_token.
	 * @param nikeEmail The users nike+ email address.
	 * @param nikePassword The users nike+ password.
	 * @return nike+ access_token..
	 * @throws IOException If we are unable to successfully authenticate with Nike+.
	 */
	public static @Nonnull String login(@Nonnull String nikeEmail, @Nonnull char[] nikePassword) throws IOException {
		Preconditions.checkNotNull(nikeEmail, "nikeEmail argument is null.");
		Preconditions.checkNotNull(nikePassword, "nikePassword argument is null.");
		Preconditions.checkArgument(isPasswordValid(nikePassword), INVALID_PASSWORD_ERROR_MESSAGE);

		// Create CookieStore for the nikeEmail request.
		CookieStore cookieStore = new BasicCookieStore();
		cookieStore.addCookie(createCookie("app", nikePlusProperties.getProperty("NIKEPLUS_APP")));
		cookieStore.addCookie(createCookie("client_id", nikePlusProperties.getProperty("NIKEPLUS_CLIENT_ID")));
		cookieStore.addCookie(createCookie("client_secret", nikePlusProperties.getProperty("NIKEPLUS_CLIENT_SECRET")));

		// Create the HttpClient, setting the cookie store.
		try (CloseableHttpClient client = HttpClients.createDefaultHttpClientBuilder()
				.setDefaultCookieStore(cookieStore)
				.build()) {
			// Create the HttpPost, set the user-agent and nike+ credentials.
			HttpPost post = new HttpPost(URL_LOGIN);
			post.addHeader("user-agent", USER_AGENT);
			post.setEntity(generateFormNameValuePairs("email", nikeEmail, "password", new String(nikePassword)));

			// Send the HTTP request.
			HttpClientContext httpClientContext = HttpClientContext.create();
			logger.out("Authenticating against Nike+");
			try (CloseableHttpResponse response = client.execute(post, httpClientContext)) {
				// Consume the response
				EntityUtils.consumeQuietly(response.getEntity());

				// Iterate through the cookies for "access_token".
				for (Cookie cookie : httpClientContext.getCookieStore().getCookies()) {
					if (cookie.getName().equals("access_token")) {
						return cookie.getValue();
					}
				}
			}
		}

		// If we reach here, we haven't got an access-token back for whatever reason.
		throw new IOException("Unable to authenticate with Nike+.<br />Please check email and nikePassword.");
	}

	/**
	 * Perform a full synchronisation cycle (check-pin-status, sync, end-sync) with nike+ for the given credentials and garmin activities.
	 * @param nikeEmail The users nike+ email address.
	 * @param nikePassword The users nike+ password.
	 * @param nikeActivitiesData Nike activities data to upload.
	 * @throws IOException If there was a problem communicating with nike+.
	 */
	@Deprecated
	public static void fullSync(@Nonnull String nikeEmail, @Nonnull char[] nikePassword, @Nonnull NikeActivityData... nikeActivitiesData) throws IOException {
		Preconditions.checkNotNull(nikeEmail, "nikeEmail argument is null.");
		Preconditions.checkNotNull(nikePassword, "nikePassword argument is null.");
		Preconditions.checkNotNull(nikeActivitiesData, "garminActivitiesData argument is null.");

		logger.out("Uploading to Nike+...");
		logger.out(" - Authenticating...");
		String nikeAccessToken = login(nikeEmail, nikePassword);
		fullSync(nikeAccessToken, nikeActivitiesData);
	}

	@Deprecated
	public static void fullSync(@Nonnull String nikeAccessToken, @Nonnull NikeActivityData... nikeActivitiesData) throws IOException {
		logger.out(" - Syncing data...");
		for (NikeActivityData nikeActivityData : nikeActivitiesData) {
			if (!syncData(nikeAccessToken, nikeActivityData)) {
				throw new IOException("There was a problem uploading to nike+.  Please try again later, if the problem persists contact me with details of the activity-id or tcx file.");
			}
		}
	}

	@Deprecated
	private static boolean syncData(@Nonnull String accessToken, @Nonnull NikeActivityData nikeActivityData) throws IOException {
		try (CloseableHttpClient client = HttpClients.createDefaultHttpClientBuilder().build()) {
			HttpPost post = new HttpPost(String.format(URL_DATA_SYNC, accessToken));
			post.addHeader("user-agent", USER_AGENT);
			post.addHeader("appid", "NIKEPLUSGPS");
			post.addHeader("Accept", "application/json");

			// Add "runXML" data to the request.
			MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create()
					.addPart("runXML", new SpoofFileBody(Util.documentToString(nikeActivityData.getRunXML()), "runXML.xml"));

			// If we have GPX data, add it to the request.
			if (nikeActivityData.getGpxXML() != null) {
				multipartEntityBuilder.addPart("gpxXML", new SpoofFileBody(Util.documentToString(nikeActivityData.getGpxXML()), "gpxXML.xml"));
			}

			post.setEntity(multipartEntityBuilder.build());
			try (CloseableHttpResponse response = client.execute(post)) {
				EntityUtils.consumeQuietly(response.getEntity());
				int statusCode = response.getStatusLine().getStatusCode();
				logger.out(Level.FINE, " - response code: %d", statusCode);
				return (URL_DATA_SYNC_SUCCESS == statusCode);
			}
		}
	}

	public static boolean syncData(@Nonnull String accessToken, @Nonnull NikePlusSyncData... nikePlusSyncDatas) throws IOException, JAXBException {
		Preconditions.checkNotNull(accessToken, "accessToken argument is null.");
		Preconditions.checkNotNull(nikePlusSyncDatas, "nikePlusSyncDatas argument is null.");
		Preconditions.checkArgument(nikePlusSyncDatas.length > 0, "No nikePlusSyncData to sync");

		// TODO: we must return some object which details which workouts succeeded/failed.
		boolean success = true;

		for (NikePlusSyncData nikePlusSyncData : nikePlusSyncDatas) {
			try (CloseableHttpClient client = HttpClients.createDefaultHttpClientBuilder().build()) {
				HttpPost post = new HttpPost(String.format(URL_DATA_SYNC, accessToken));
				post.addHeader("user-agent", USER_AGENT);
				post.addHeader("appid", "NIKEPLUSGPS");
				post.addHeader("Accept", "application/json");

				try (StringWriter stringWriter = new StringWriter()) {
					JAXBObject.GPX_TYPE.marshal(new ObjectFactory().createGpx(nikePlusSyncData.getGpxXML()), stringWriter);

					HttpEntity httpEntity = MultipartEntityBuilder.create()
							.addTextBody("run", new Gson().toJson(nikePlusSyncData.getRunJson()), ContentType.APPLICATION_JSON)
							.addTextBody("gpxXML", stringWriter.toString(), ContentType.TEXT_PLAIN)
							.build();
					post.setEntity(httpEntity);

					logger.out("Posting to Nike+");
					try (CloseableHttpResponse response = client.execute(post)) {
						int statusCode = response.getStatusLine().getStatusCode();

						nikePlusSyncData.setResponseEntityContent(EntityUtils.toString(response.getEntity()));
						nikePlusSyncData.setResponseStatusCode(statusCode);
						EntityUtils.consumeQuietly(response.getEntity());
						logger.out(Level.FINE, " - response code: %d", statusCode);
						if (statusCode != URL_DATA_SYNC_SUCCESS) {
							success = false;
						}
					}
				}
			}
		}

		return success;
	}

	public static void endSync(@Nonnull String accessToken) throws IOException {
		Preconditions.checkNotNull(accessToken, "accessToken argument is null.");

		try (CloseableHttpClient client = HttpClients.createDefaultHttpClientBuilder()
				.setDefaultRequestConfig(RequestConfig.custom()
						.setConnectTimeout(16_000)
						.setConnectionRequestTimeout(16_000)
						.setSocketTimeout(32_000)
						.build())
				.build()) {
			HttpPost post = new HttpPost(String.format("%s?%s", URL_DATA_SYNC_COMPLETE_ACCESS_TOKEN, Util.generateHttpParameter("access_token", accessToken)));
			post.addHeader("user-agent", USER_AGENT);
			post.addHeader("appId", "NIKEPLUSGPS");

			logger.out("Ending Nike+ sync");
			try (CloseableHttpResponse response = client.execute(post)) {
				logger.out(Level.FINE, " - response code: %d", response.getStatusLine().getStatusCode());
				HttpEntity httpEntity = response.getEntity();
				if (httpEntity != null) {
					try (InputStream inputStream = httpEntity.getContent()) {
						Document outDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream);
						outDoc.normalize();
						logger.out(Level.FINER, "\t%s", Util.documentToString(outDoc));
					} catch (ParserConfigurationException | SAXException e) {
						logger.out(e);
					} finally {
						EntityUtils.consumeQuietly(httpEntity);
					}
				} else {
					throw new NullPointerException("Http response empty");
				}
			}
		}
	}
} public enum JAXBObject {
	GPX_TYPE {
		@Override
		@Nonnull Class[] getClassesToBeBound() {
			return new Class[] { GpxType.class, TrackPointExtensionT.class };
		}
	},

	TRAINING_CENTER_DATABASE {
		@Override
		@Nonnull Class[] getClassesToBeBound() {
			return new Class[] { TrainingCenterDatabaseT.class };
		}
	};

	abstract @Nonnull Class[] getClassesToBeBound();

	private final JAXBContext JAXB_CONTEXT = createJAXBContect();
	private @Nonnull JAXBContext createJAXBContect() {
		try {
			return JAXBContext.newInstance(getClassesToBeBound());
		} catch (JAXBException je) {
			throw new ExceptionInInitializerError(je);
		}
	}

	private final @Nonnull ThreadLocal<Marshaller> MARSHALLER = new ThreadLocal<Marshaller>() {
		protected synchronized Marshaller initialValue() {
			try {
				Marshaller marshaller = JAXB_CONTEXT.createMarshaller();
				marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
				return marshaller;
			} catch (JAXBException e) {
				throw new ExceptionInInitializerError(e);
			}
		}
	};

	private final @Nonnull ThreadLocal<Unmarshaller> UNMARSHALLER = new ThreadLocal<Unmarshaller>() {
		protected synchronized Unmarshaller initialValue() {
			try {
				return JAXB_CONTEXT.createUnmarshaller();
			} catch (JAXBException e) {
				throw new ExceptionInInitializerError(e);
			}
		}
	};

	private @Nonnull Marshaller getMarshaller() {
		return MARSHALLER.get();
	}

	private @Nonnull Unmarshaller getUnmarshaller() {
		return UNMARSHALLER.get();
	}

	public void marshal(@Nonnull JAXBElement<?> jaxbElement, @Nonnull StringWriter stringWriter) throws JAXBException {
		Preconditions.checkNotNull(jaxbElement, "jaxbElement argument is null.");
		Preconditions.checkNotNull(stringWriter, "stringWriter argument is null.");
		getMarshaller().marshal(jaxbElement, stringWriter);
	}

	@Deprecated
	public void marshal(@Nonnull JAXBElement<?> jaxbElement, @Nonnull Document document) throws JAXBException {
		Preconditions.checkNotNull(jaxbElement, "jaxbElement argument is null.");
		Preconditions.checkNotNull(document, "document argument is null.");
		getMarshaller().marshal(jaxbElement, document);
	}

	public @Nonnull <T> T unmarshall(@Nonnull InputStream inputStream) throws JAXBException {
		Preconditions.checkNotNull(inputStream, "inputStream argument is null.");

		//noinspection unchecked
		return (T) getUnmarshaller().unmarshal(new StreamSource(inputStream), getClassesToBeBound()[0]).getValue();
	}
} public class NikeActivityData {
	private final @Nonnull Document runXML;
	private final @Nullable Document gpxXML;

	public NikeActivityData(@Nonnull Document runXML, @Nullable Document gpxXML) {
		this.runXML = Preconditions.checkNotNull(runXML, "runXML argument is null.");
		this.gpxXML = gpxXML;
	}

	public @Nonnull Document getRunXML() {
		return runXML;
	}

	public @Nullable Document getGpxXML() {
		return gpxXML;
	}
} public class NikePlusSyncData {
	private final @Nonnull JsonElement runJson;
	private final @Nonnull GpxType gpxXML;
	private @Nullable String responseEntityContent;
	private int responseStatusCode = -1;

	public NikePlusSyncData(@Nonnull JsonElement runJson, @Nonnull GpxType gpxXML) {
		this.runJson = Preconditions.checkNotNull(runJson, "RunJson argument is null.");
		this.gpxXML = Preconditions.checkNotNull(gpxXML, "gpxXML argument is null.");
	}

	public @Nonnull JsonElement getRunJson() {
		return runJson;
	}

	public @Nonnull GpxType getGpxXML() {
		return gpxXML;
	}

	public @Nullable String getResponseEntityContent() {
		return responseEntityContent;
	}

	public void setResponseEntityContent(@Nullable String responseEntityContent) {
		this.responseEntityContent = responseEntityContent;
	}

	public int getResponseStatusCode() {
		return responseStatusCode;
	}

	public void setResponseStatusCode(int responseStatusCode) {
		this.responseStatusCode = responseStatusCode;
	}
} public class RunJson {
	private final @Nonnull BigDecimal distance;
	private final @Nonnull BigDecimal duration;
	private final long startTime;
	private final @Nonnull String status;
	private final @Nonnull String timeZoneId;
	private final @Nonnull String type;
	private final @Nonnull List<Detail> detail;
	private final @Nonnull Summary summary;

	public RunJson(@Nonnull BigDecimal distance,
				   @Nonnull BigDecimal duration,
				   long startTime,
				   @Nonnull String status,
				   @Nonnull String timeZoneId,
				   @Nonnull String type,
				   @Nonnull List<Detail> detail,
				   @Nonnull Summary summary) {
		this.distance = Preconditions.checkNotNull(distance, "distance argument is null.");
		this.duration = Preconditions.checkNotNull(duration, "duration argument is null.");
		this.startTime = startTime;
		this.status = Preconditions.checkNotNull(status, "status argument is null.");
		this.timeZoneId = Preconditions.checkNotNull(timeZoneId, "timeZoneId argument is null.");
		this.type = Preconditions.checkNotNull(type, "type argument is null.");
		this.detail = Preconditions.checkNotNull(detail, "detail argument is null.");
		this.summary = Preconditions.checkNotNull(summary, "summary argument is null.");
	}


	public @Nonnull BigDecimal getDistance() {
		return distance;
	}

	public @Nonnull BigDecimal getDuration() {
		return duration;
	}

	public static class Detail {
		private final @Nonnull String metricType;
		private final @Nonnull String intervalType;
		private final @Nonnull String intervalUnit;
		private final long startTimeOffset;
		private final @Nonnull String intervalMetric;
		private final @Nonnull String objType;
		private final @Nonnull List<BigDecimal> value;

		public Detail(@Nonnull String metricType,
					  @Nonnull String intervalType,
					  @Nonnull String intervalUnit,
					  long startTimeOffset,
					  @Nonnull String intervalMetric,
					  @Nonnull String objType,
					  @Nonnull List<BigDecimal> value) {
			this.metricType = Preconditions.checkNotNull(metricType, "metricType argument is null.");
			this.intervalType = Preconditions.checkNotNull(intervalType, "intervalType argument is null.");
			this.intervalUnit = Preconditions.checkNotNull(intervalUnit, "intervalUnit argument is null.");
			this.startTimeOffset = startTimeOffset;
			this.intervalMetric = Preconditions.checkNotNull(intervalMetric, "intervalMetric argument is null.");
			this.objType = Preconditions.checkNotNull(objType, "objType argument is null.");
			this.value = Preconditions.checkNotNull(value, "value argument is null.");
		}
	}

	public static class Summary {
		private final @Nonnull List<Snapshot> snapshots;
		private final @Nonnull List<DeviceConfig> deviceConfig;

		public Summary(@Nonnull List<Snapshot> snapshots, @Nonnull List<DeviceConfig> deviceConfig) {
			this.snapshots = Preconditions.checkNotNull(snapshots, "value argument is null.");
			this.deviceConfig = Preconditions.checkNotNull(deviceConfig, "value argument is null.");
		}

		public static class Snapshot {
			private final @Nonnull String name;
			private final @Nonnull List<DataSeries> dataSeries;

			public Snapshot(@Nonnull String name, @Nonnull List<DataSeries> dataSeries) {
				this.name = Preconditions.checkNotNull(name, "value argument is null.");
				this.dataSeries = Preconditions.checkNotNull(dataSeries, "dataSeries argument is null.");
			}

			public static class DataSeries {
				private final @Nonnull Metrics metrics;
				private final @Nonnull String objType;

				public DataSeries(@Nonnull Metrics metrics, @Nonnull String objType) {
					this.metrics = Preconditions.checkNotNull(metrics, "metrics argument is null.");
					this.objType = Preconditions.checkNotNull(objType, "objType argument is null.");
				}

				public static class Metrics {
					private final int distance;
					private final long duration;

					public Metrics(int distance, long duration) {
						this.distance = distance;
						this.duration = duration;
					}
				}
			}
		}

		public static class DeviceConfig {
			private final @Nonnull Component component;

			public DeviceConfig(@Nonnull Component component) {
				this.component = Preconditions.checkNotNull(component, "component argument is null.");
			}

			public static class Component {
				private final @Nonnull String type;
				private final @Nonnull String category;

				public Component(@Nonnull String type, @Nonnull String category) {
					this.type = Preconditions.checkNotNull(type, "type argument is null.");
					this.category = Preconditions.checkNotNull(category, "category argument is null.");
				}
			}
		}
	}
} public class Log {
	private static Log _context;
	private static Logger _log;

	private static final @Nonnull Predicate<StackTraceElement> CALLER_PREDICATE = new Predicate<StackTraceElement>() {
		@Override
		public boolean apply(@Nullable StackTraceElement element) {;
			return element != null && !(element.getClassName().equals(Thread.class.getName()) || element.getClassName().equals(Log.class.getName()));
		}
	};

	private Log() {
		_log = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
	}

	public static synchronized Log getInstance() {
		if (_context == null)
			_context = new Log();

		return _context;
	}

	public void out(Object message) {
		out(Level.INFO, null, message.toString(), (Object[])null);
	}

	public void out(String message, Object ... args) {
		out(Level.INFO, null, message, args);
	}

	public void out(Level level, Object message) {
		out(level, null, message.toString(), (Object[])null);
	}

	public void out(Level level, String message, Object ... args) {
		out(level, null, message, args);
	}

	public void out(Throwable throwable) {
		out(Level.SEVERE, throwable, "", (Object[])null);
	}

	public void out(Level level, Throwable throwable, Object message) {
		out(level, throwable, message.toString(), (Object[])null);
	}

	public void out(Level level, Throwable throwable, String message, Object ... args) {
		if (_log.isLoggable(level)) {
			if (args != null) {
				message = String.format(message, args);
			}

			// Get the caller class and method name (this is a *disgusting* hack which I've been forced into because I've
			// mis-used java.util.logging.  Maybe I'll fix this one day, but it's not important for now.
			StackTraceElement caller = Iterables.find(Arrays.asList(Thread.currentThread().getStackTrace()), CALLER_PREDICATE);
			_log.logp(level, caller.getClassName(), caller.getMethodName(), message, throwable);
		}
	}
} public class Util {

	private static final int Connection_Timeout = 12 * 1000;


	private static final Log log = Log.getInstance();


	public static String getSimpleNodeValue(Document doc, String nodeName) {
		return getSimpleNodeValue(doc.getElementsByTagName(nodeName).item(0));
	}

	public static String getSimpleNodeValue(Node node) {
		return node.getChildNodes().item(0).getNodeValue();
	}

	public static Calendar getCalendarNodeValue(Node node) throws DatatypeConfigurationException {
		return getCalendarValue(getSimpleNodeValue(node));
	}

	public static Calendar getCalendarValue(String value) throws DatatypeConfigurationException {
		return DatatypeFactory.newInstance().newXMLGregorianCalendar(value).toGregorianCalendar();
	}


	public static Node getFirstChildByNodeName(Node parent, String nodeName) {
		for (Node n = parent.getFirstChild(); n != null; n = n.getNextSibling())
			if (n.getNodeName().equals(nodeName)) return n;

		return null;
	}

	public static Node[] getChildrenByNodeName(Node parent, String nodeName) {
		ArrayList<Node> al = new ArrayList<Node>();

		for (Node n = parent.getFirstChild(); n != null; n = n.getNextSibling())
			if (n.getNodeName().equals(nodeName)) al.add(n);

		return (al.size() > 0) ? al.toArray(new Node[0]) : null;
	}



	public static Element appendElement(Node parent, String name) {
		return appendElement(parent, name, null);
	}

	public static Element appendElement(Node parent, String name, Object data, String ... attributes) {
		Document doc = (parent.getNodeType() == Node.DOCUMENT_NODE) ? (Document)parent : parent.getOwnerDocument();
		Element e = doc.createElement(name);
		parent.appendChild(e);

		if (data != null) e.appendChild(doc.createTextNode(data.toString()));

		for (int i = 0; i < attributes.length; ++i)
			e.setAttribute(attributes[i++], attributes[i]);

		return e;
	}

	public static void appendCDATASection(Node parent, String name, Object data) {
		Document doc = (parent.getNodeType() == Node.DOCUMENT_NODE) ? (Document)parent : parent.getOwnerDocument();
		Element e = doc.createElement(name);
		parent.appendChild(e);
		e.appendChild(doc.createCDATASection(data.toString()));
	}

	public static String documentToString(Document doc) {
		try {
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			DOMSource source = new DOMSource(doc);

			Writer outWriter = new StringWriter();
			StreamResult result = new StreamResult(outWriter);
			transformer.transform(source, result);
			return outWriter.toString();
		}
		catch (Exception e) {
			log.out(e);
			return null;
		}
	}

	/*
	public static void printDocument(Document doc) {
		try {
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			DOMSource source = new DOMSource(doc);
			StreamResult result =  new StreamResult(System.out);
			transformer.transform(source, result);
			//System.out.println();
		}
		catch (Exception e) {
			log.out(e);
		}
	}
	*/




	public static void writeDocument(Document doc, String filename) {
		Source source = new DOMSource(doc);
		File file = new File(filename);
		Result result = new StreamResult(file);

		try {
			Transformer xformer = TransformerFactory.newInstance().newTransformer();
			xformer.transform(source, result);
		}
		catch (TransformerConfigurationException ex) {
			log.out(Level.SEVERE, null, ex);
		}
		catch (TransformerException ex) {
			log.out(ex);
		}
	}


	public static String generateHttpParameter(String key, String val) throws UnsupportedEncodingException {
		return String.format("%s=%s", URLEncoder.encode(key, "UTF-8"), URLEncoder.encode(val, "UTF-8"));
	}

	public static Document downloadFile(String location) throws MalformedURLException, IOException, ParserConfigurationException, SAXException {
		return downloadFile(location, null);
	}

	public static Document downloadFile(String location, String parameters) throws MalformedURLException, IOException, ParserConfigurationException, SAXException, FileNotFoundException, SocketTimeoutException {
		URL url = new URL(location);
		URLConnection conn = url.openConnection();

		if (parameters != null) {
			// Use post mode
			conn.setDoOutput(true);
			conn.setAllowUserInteraction(false);
			conn.setConnectTimeout(Connection_Timeout);

			// Send query
			PrintStream ps = new PrintStream(conn.getOutputStream());
			ps.print(parameters);
			ps.close();
		}
		
		// Get response
		Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(conn.getInputStream());
		if ((doc != null) && (doc.getDocumentElement() != null))
			doc.getDocumentElement().normalize();

		return doc;
	}


	public static Document generateDocument(File file) throws ParserConfigurationException, SAXException, IOException {
		Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file);
		if ((doc != null) && (doc.getDocumentElement() != null))
			doc.getDocumentElement().normalize();

		return doc;
	}

	public static Document generateDocument(String string) throws ParserConfigurationException, SAXException, IOException {
		Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(string)));
		if ((doc != null) && (doc.getDocumentElement() != null))
			doc.getDocumentElement().normalize();

		return doc;
	}
	
	
	
	private final static String TRAINING_CENTER_DATABASE = "<TrainingCenterDatabase " +
			"xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\" " +
			"xmlns:ns5=\"http://www.garmin.com/xmlschemas/ActivityGoals/v1\" " +
			"xmlns:ns3=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\" " +
			"xmlns:ns2=\"http://www.garmin.com/xmlschemas/UserProfile/v2\" " +
			"xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" " +
			"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:ns4=\"http://www.garmin.com/xmlschemas/ProfileExtension/v1\"></TrainingCenterDatabase>";
	
	public static Document[] parseMultipleWorkouts(File inFile) throws ParserConfigurationException, SAXException, IOException {
		return parseMultipleWorkouts(generateDocument(inFile));
	}
		
	public static Document[] parseMultipleWorkouts(Document doc) throws ParserConfigurationException, SAXException, IOException {
		NodeList activities = doc.getElementsByTagName("Activity");
		int activitiesLength = activities.getLength();
		
		if (activitiesLength == 1) return new Document[] { doc };
		else {
			Document[] docs = new Document[activitiesLength];
			
			for (int i = 0; i < activitiesLength; ++i) {
				Node n = activities.item(i);
				
				Document activity = generateDocument(TRAINING_CENTER_DATABASE);
				Node imported = activity.importNode(n, true);
				activity.getFirstChild().appendChild(imported);
				
				docs[i] = activity;
			}
			
			return docs;
		}
	}
}
import com.awsmithson.tcx2nikeplus.convert.ConvertGpx;
import com.awsmithson.tcx2nikeplus.convert.ConvertTcx;
import com.awsmithson.tcx2nikeplus.convert.ConverterException;
import com.awsmithson.tcx2nikeplus.convert.GpxToRunJson;
import com.awsmithson.tcx2nikeplus.garmin.GarminActivityData;
import com.awsmithson.tcx2nikeplus.garmin.GarminDataType;
import com.awsmithson.tcx2nikeplus.http.Garmin;
import com.awsmithson.tcx2nikeplus.http.NikePlus;
import com.awsmithson.tcx2nikeplus.jaxb.JAXBObject;
import com.awsmithson.tcx2nikeplus.nike.NikeActivityData;
import com.awsmithson.tcx2nikeplus.nike.NikePlusSyncData;
import com.awsmithson.tcx2nikeplus.nike.RunJson;
import com.awsmithson.tcx2nikeplus.util.Log;
import com.awsmithson.tcx2nikeplus.util.Util;
import com.garmin.xmlschemas.trainingcenterdatabase.v2.ObjectFactory;
import com.garmin.xmlschemas.trainingcenterdatabase.v2.TrainingCenterDatabaseT;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.topografix.gpx._1._1.GpxType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.util.List;
import java.util.logging.Level;
import javax.annotation.Nonnull;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
package com.awsmithson.tcx2nikeplus.servlet;




public enum ServletUploadDataType {
	GARMIN_ACTIVITY_ID {
		@Override
		void process(@Nonnull HttpServletRequest request,
					 @Nonnull PrintWriter out,
					 @Nonnull String nikeAccessToken) throws IOException, ConverterException {
			int garminActivityId = Servlets.getGarminActivityId(Servlets.getRequiredParameter(request, PARAMETER_GARMIN_ACTIVITY_ID));
			logger.out("Received convert-activity-id request, id: %d", garminActivityId);

			// If that failed for whatever reason (it is new/beta), use our legacy method.
			ImmutableList.Builder<GarminActivityData> garminActivities = ImmutableList.builder();
			try (CloseableHttpClient client = GarminDataType.getGarminHttpSession()) {
				garminActivities.add(new GarminActivityData(Garmin.downloadGarminTcx(client, garminActivityId), Garmin.downloadGarminGpx(client, garminActivityId)));
			}
			processGarminActivityData(request, garminActivities.build(), nikeAccessToken, out);
		}
	},
	GPX_FILE {
		@Override
		void process(@Nonnull HttpServletRequest request, @Nonnull PrintWriter out, @Nonnull String nikeAccessToken) throws IOException, ServletException, JAXBException, ConverterException {
			Part part = request.getPart(PART_GPX_FILE);
			logger.out("Received gpx-file request: %s (%d bytes)", part.getSubmittedFileName(), part.getSize());

			try (InputStream inputStream = part.getInputStream()) {
				GpxType gpxXml = GarminDataType.GPX.unmarshall(inputStream);
				processGpx(gpxXml, nikeAccessToken, out);
			}
		}
	},
	TCX_FILE {
		@Override
		void process(@Nonnull HttpServletRequest request,
					 @Nonnull PrintWriter out,
					 @Nonnull String nikeAccessToken) throws IOException, ConverterException, ServletException, JAXBException, ParserConfigurationException, SAXException {
			Part part = request.getPart(PART_GARMIN_TCX_FILE);
			logger.out("Received tcx-file request: %s (%d bytes)", part.getSubmittedFileName(), part.getSize());

			try (InputStream inputStream = part.getInputStream()) {
				// Use JAXB to unmarshall...  We could create an org.w3c.dom.Document directly from the InputStream,
				// but we'll be getting rid of all usage of org.w3c.dom.Document soon, and this will be easier to modify
				// when that time comes.
				TrainingCenterDatabaseT garminTcx = GarminDataType.TCX.unmarshall(inputStream);
				JAXBElement<TrainingCenterDatabaseT> jaxbElement = new ObjectFactory().createTrainingCenterDatabase(garminTcx);
				Document tcxDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
				JAXBObject.TRAINING_CENTER_DATABASE.marshal(jaxbElement, tcxDocument);

				// Create a list of GarminActivityData to upload to nike+.
				ImmutableList.Builder<GarminActivityData> garminActivities = ImmutableList.builder();
				for (Document doc : Util.parseMultipleWorkouts(tcxDocument)) {
					garminActivities.add(new GarminActivityData(doc, null));
				}
				processGarminActivityData(request, garminActivities.build(), nikeAccessToken, out);
			}
		}
	};


	private static final Log logger = Log.getInstance();

	private static final String PART_GPX_FILE = "gpxFile";
	private static final String PART_GARMIN_TCX_FILE = "garminTcxFile";
	private static final String PARAMETER_GARMIN_ACTIVITY_ID = "garminActivityId";
	private static final String PARAMETER_CLIENT_TIME_ZONE_OFFSET = "clientTimeZoneOffset";

	private static final String LOG_PROCESSING_FORMAT_STRING = "Processing (GPX beta)";
	private static final String LOG_SUCCESS_FORMAT_STRING = "Conversion & Upload Successful (GPX beta)";
	private static final String LOG_FAILED_FORMAT_STRING = "Conversion & Upload Failed (GPX beta)";

	abstract void process(@Nonnull HttpServletRequest request,
						  @Nonnull PrintWriter out,
						  @Nonnull String nikeAccessToken) throws IOException, ConverterException, ServletException, JAXBException, ParserConfigurationException, SAXException;

	void process(@Nonnull HttpServletRequest request,
				 @Nonnull PrintWriter out,
				 @Nonnull String nikeEmail,
				 @Nonnull char[] nikePassword) throws IOException, SAXException, ParserConfigurationException, ConverterException, ServletException, JAXBException {
		if (NikePlus.isPasswordValid(nikePassword)) {
			// Log into nikeplus, process our input and always end-sync to end our session with Nike+.
			String nikeAccessToken = NikePlus.login(nikeEmail, nikePassword);
			try {
				process(request, out, nikeAccessToken);
			} finally {
				NikePlus.endSync(nikeAccessToken);
			}
		} else {
			ConvertServlet.fail(out, NikePlus.INVALID_PASSWORD_ERROR_MESSAGE);
		}
	}

	@Beta
	private static boolean processGpx(int garminActivityId,
									  @Nonnull String nikeAccessToken,
									  @Nonnull PrintWriter out) {
		try (CloseableHttpClient closeableHttpClient = GarminDataType.getGarminHttpSession()) {
			GpxType gpxXml = GarminDataType.GPX.downloadAndUnmarshall(closeableHttpClient, garminActivityId);
			return processGpx(gpxXml, nikeAccessToken, out);
		} catch (Throwable t) {
			logger.out(Level.WARNING, t, String.format(LOG_FAILED_FORMAT_STRING));
			return false;
		}
	}

	@Beta
	private static boolean processGpx(@Nonnull GpxType gpxXml,
									  @Nonnull String nikeAccessToken,
									  @Nonnull PrintWriter out) throws ConverterException, IOException, JAXBException {
		logger.out(LOG_PROCESSING_FORMAT_STRING);
		RunJson runJson = new GpxToRunJson().convert(gpxXml);
		JsonElement runJsonElement = new Gson().toJsonTree(runJson);

		NikePlusSyncData nikePlusSyncData = new NikePlusSyncData(runJsonElement, gpxXml);
		boolean success = NikePlus.syncData(nikeAccessToken, nikePlusSyncData);
		if (success) {
			logger.out(LOG_SUCCESS_FORMAT_STRING);
			String message = "Conversion & Upload Successful.";
			String nikeActivityId = null;

			// Attempt to generate something like "view your workout at https://secure-nikeplus.nike.com/plus/activity/running/detail/<workout-id>
			String responseData = nikePlusSyncData.getResponseEntityContent();
			if (responseData != null) {
				try {
					logger.out(Level.FINER, " - Nike+ sync response: %s", responseData);
					nikeActivityId = Util.getSimpleNodeValue(Util.generateDocument(responseData), "activityId");
				} catch (Throwable t) {
					logger.out(Level.WARNING, t, "Ignoring exception - occured whilst trying to get nike+ activity-id.");
				}
			}

			ConvertServlet.succeed(out, message, nikeActivityId, runJson.getDuration().longValue(), runJson.getDistance().multiply(new BigDecimal("1000")).doubleValue());
		} else {
			logger.out(Level.WARNING, LOG_FAILED_FORMAT_STRING);
		}
		return success;
	}

	@Deprecated
	private static void processGarminActivityData(@Nonnull HttpServletRequest request,
												  @Nonnull List<GarminActivityData> garminActivities,
												  @Nonnull String nikeAccessToken,
												  @Nonnull PrintWriter out) throws ConverterException, IOException {
		Preconditions.checkArgument(garminActivities.size() <= 25, String.format("Exceeded maximum activities per upload (25).  Your TCX file contains %d activities.", garminActivities.size()));

		ConvertTcx convertTcx = new ConvertTcx();