PageSourceSearch

https://joburgnorthcitymarathon.co.za/wp-content/plugins/gpx-viewer/js/gpxCleaner.js

js joburgnorthcitymarathon.co.za collected 2026-10-09 00:52:14 UTC 30,879 bytes, 958 lines download raw bytes

1/**
2 *-----------------------------------------------------------------------------
3 * gpxCleaner.js
4 *
5 * 		GPX Track Cleaner
6 *			- reducing track points very close to each other (Douglas-Peucker Algorithm)
7 *			- smoothing elevation data (from Peter Danninger, [email protected])
8 *			- replacing elevation data using Open-Elevation online service
9 *
10 *		part of WordPress plugin GPX Viewer
11 *
12 * @author	axelkeller
13 *
14 *-----------------------------------------------------------------------------
15 * A new GPX file is produced from waypoints and tracks (no routes)
16 * that contains only geo data like <lat>, <lon>, <ele> and <time>.
17 *
18 * Spikes of elevation data resp. missing values can be adjusted.
19 * Additionally the elevation profile is smoothed using the adjacent points. 
20 *
21 * Tracking of elevations data with mobile devices often results in poor values.
22 * On demand the elevation data can be replaced by SRTM-values (Shuttle Radar Topography Mission)
23 * using the Open-Elevation Online Elevation Service.
24 *-----------------------------------------------------------------------------
25**/
26var gpxc = gpxc || {};							// Namespace
27
28//-----------------------------------------------------------------------------
29
30gpxc._loadedText	= "";						// Content of original GPX file
31gpxc._attributes	= "";						// Attributes of original GPX file
32gpxc._wpt_extension	= "";						// Waypoint extension
33gpxc._trk_extension	= "";						// Track extension
34gpxc._xmlDOM		= null;						// GPX file transformed in a Document Object Model (DOM)
35
36gpxc._wpts			= new Array();				// way points
37gpxc._trkName		= "";						// <name>
38gpxc._track			= null;						// parsed and cleaned track
39
40gpxc._meterJeGradLat = 40075017 / 360;			// 111.319,49 m (fix)
41gpxc._meterJeGradLon = function(lat) {			// depending on lat
42	return (gpxc._meterJeGradLat * Math.cos (lat * Math.PI / 180));
43}
44
45
46/**
47 *	Load GPX file from local computer and transform in a DOM-Object (gpxc._xmlDOM),
48 *	if the XML structure of the file is o.k., then parse the DOM-Object and generate an javascript object (gpxc._track)
49 *	
50 *	@param	file		file descriptor
51 *	@param	callback	callback function when asynchronous loading and parsing have been finished
52 *	@return	true/false	if operation was successful
53**/
54gpxc.loadFile = function(file, callback) {
55	var loadedText = "";									// for the loaded content
56	
57	var fileReader = new FileReader ();
58	
59	fileReader.onload = function(fileLoadedEvent) {			// when GPX file has been loaded
60		loadedText = fileLoadedEvent.target.result;			// content of the GPX file
61		
62		if (gpxc.parseFile(loadedText))
63			callback(loadedText);							// loading and parsing have been finished
64	}
65	fileReader.readAsText (file, "UTF-8");
66	
67	// No waiting here for end of asnchronous "fileReader.onload"
68}
69/**
70 *	Parse GPX input and transform it to a DOM-Object (gpxc._xmlDOM),
71 *	if the XML structure of the file is o.k., then parse the DOM-Object and generate an javascript object (gpxc._track)
72 *	
73 *	@param	loadedText	loaded text of the GPX file
74 *	@return	true/false	if operation was successful
75**/
76gpxc.parseFile = function(loadedText) {
77	gpxc._loadedText = loadedText;
78	loadedText = loadedText.replace(/>\s+</g,"><");		// clean text
79	
80	gpxc._xmlDOM = null;								// DOM object (tree structure)
81	
82	if (typeof ActiveXObject != "undefined" && typeof GetObject != "undefined") {
83		gpxc._xmlDOM = new ActiveXObject ("Microsoft.xmlDOM");
84		xmlDOM.loadXML (loadedText);					// IExplore workaround
85	}
86
87	if (typeof DOMParser != "undefined")
88		gpxc._xmlDOM = (new DOMParser ()).parseFromString(loadedText, "text/xml");
89
90	if (typeof gpxc._xmlDOM == "object" && gpxc._xmlDOM != null)
91		gpxc.parseGPX();								// parse loaded file
92	else
93		return false;
94	
95	// save and extend gpx attributes
96	i = loadedText.indexOf('<gpx ');
97	if (i >= 0) {
98		j = loadedText.indexOf('>', i);
99		if (j > i) {
100			gpxc._attributes = loadedText.substr(i+5, j-i-5);
101			
102			var attributes = gpxc._attributes;
103			i = attributes.indexOf('modifier="');
104			if (i >= 0) {
105				j = attributes.indexOf('"', i+10);
106				if (j > i)
107					gpxc._attributes = attributes.substr(0, i) + attributes.substr(j+1);
108			}
109			var addon = '; modified by GPX Track Cleaner from GPX Viewer plugin (WordPress) ';
110			var attributes = gpxc._attributes;
111			i = attributes.indexOf(addon);
112			if (i < 0) {
113				i = attributes.indexOf('creator="');
114				if (i >= 0) {
115					j = attributes.indexOf('"', i+9);
116					if (j > i)
117						gpxc._attributes =	attributes.substr(0, j) + addon + attributes.substr(j);
118				}
119			}
120		}
121	}
122
123	// save waypoint extensions
124	i = loadedText.indexOf('<wpt ');
125	if (i >= 0) {
126		j = loadedText.indexOf('</wpt>', i+5);
127		if (j > i) {
128			var wpt = loadedText.substr(i+5, j-i-5);
129			i = wpt.indexOf('<extensions>');
130			k = wpt.indexOf('<extensions>', i+12);
131			if (i >= 0) {
132				j = wpt.indexOf('</extensions>');
133				if (j > 0) {
134					while (k > 0 && k < j) {
135						j = wpt.indexOf('</extensions>', j+12);
136						k = wpt.indexOf('<extensions>', k+12);
137					}
138					gpxc._wpt_extension = wpt.substr(i+12, j-i-12);
139				}
140			}
141		}
142	}
143	
144	// save track extensions
145	i = loadedText.indexOf('<trk>');
146	if (i >= 0) {
147		j = loadedText.indexOf('</trk>', i+5);
148		if (j > i) {
149			var trk = loadedText.substr(i+5, j-i-5);
150			i = trk.indexOf('<extensions>');
151			k = trk.indexOf('<extensions>', i+12);
152			if (i >= 0) {
153				j = trk.indexOf('</extensions>');
154				if (j > 0) {
155					while (k > 0 && k < j) {
156						j = trk.indexOf('</extensions>', j+12);
157						k = trk.indexOf('<extensions>', k+12);
158					}
159					gpxc._trk_extension = trk.substr(i+12, j-i-12);
160				}
161			}
162		}
163	}
164	return true;
165}
166
167/**
168 * Parse the loaded GPX file and build up data objects
169**/
170gpxc.parseGPX = function() {
171	var lat;								// latitude: - 90° (Soth pole) ... + 90° (North pole)
172	var lon;								// longitude:  -180° (West) ... +180° (East)
173	var ele;								// Elevation [m]
174	var time;								// Timp stamp
175	gpxc._wpts		= new Array ();			// cleaned way points
176	gpxc._trkName	= "";					// Track name
177	gpxc._track		= null;					// cleaned tracks
178
179	//
180	// parse way points ---------------------------------------------------------
181	//
182	// all way points of the GPX file
183	var wpts = gpxc._xmlDOM.documentElement.getElementsByTagName ("wpt");
184
185	for (var w = 0; w < wpts.length; w++) {				// loop over all waypoints ...
186		lat = parseFloat (wpts[w].getAttribute ("lat"));
187		lon = parseFloat (wpts[w].getAttribute ("lon"));
188		ele = "--";
189		if (typeof (wpts[w].getElementsByTagName ("ele")[0]) == "object") {
190			if (wpts[w].getElementsByTagName ("ele")[0].firstChild != null) {
191				ele = parseFloat (wpts[w].getElementsByTagName ("ele")[0].firstChild.data);
192				ele = +ele.toFixed (1);
193				if (isNaN (ele))
194					ele = "--";
195			}
196		}
197		var tim = "";
198		if (typeof (wpts[w].getElementsByTagName ("time")[0]) == "object") {
199			if (wpts[w].getElementsByTagName ("time")[0].firstChild != null) {
200				tim = wpts[w].getElementsByTagName ("time")[0].firstChild.data;
201				tim = gpxc.cleanTxt(tim);
202			}
203		}
204		var nam = "";
205		if (typeof (wpts[w].getElementsByTagName ("name")[0]) == "object") {
206			if (wpts[w].getElementsByTagName ("name")[0].firstChild != null) {
207				nam = wpts[w].getElementsByTagName ("name")[0].firstChild.data;
208				nam = gpxc.cleanTxt(nam);
209			}
210		}
211		var cmt = "";
212		if (typeof (wpts[w].getElementsByTagName ("cmt")[0]) == "object") {
213			if (wpts[w].getElementsByTagName ("cmt")[0].firstChild != null) {
214				cmt = wpts[w].getElementsByTagName ("cmt")[0].firstChild.data;
215				cmt = gpxc.cleanTxt(cmt);
216			}
217		}
218		var desc = "";
219		if (typeof (wpts[w].getElementsByTagName ("desc")[0]) == "object") {
220			if (wpts[w].getElementsByTagName ("desc")[0].firstChild != null) {
221				desc = wpts[w].getElementsByTagName ("desc")[0].firstChild.data;
222				desc = gpxc.cleanTxt(desc);
223			}
224		}
225		var sym = "";
226		if (typeof (wpts[w].getElementsByTagName ("sym")[0]) == "object") {
227			if (wpts[w].getElementsByTagName ("sym")[0].firstChild != null) {
228				sym = wpts[w].getElementsByTagName ("sym")[0].firstChild.data;
229				sym = gpxc.cleanTxt(sym);
230			}
231		}
232		if (nam == "" && cmt != "")
233			nam = cmt;
234		if (nam == "" && desc != "")
235			nam = desc;
236		if (desc == cmt)
237			desc = "";
238		if (cmt == nam)
239			cmt = "";
240		if (isNaN (lat) || isNaN (lon) || nam == "")	// invalid way point is skipped
241			continue;
242
243		var wpt = new Array (8);	// way point: lat, lon, ele, name, cmt, desc, sym, time
244		wpt[0] = lat;
245		wpt[1] = lon;
246		wpt[2] = ele;
247		wpt[3] = tim;
248		wpt[4] = nam;
249		wpt[5] = cmt;
250		wpt[6] = desc;
251		wpt[7] = sym;
252		gpxc._wpts[w] = wpt;
253	}
254	
255	//
256	// Parse first track -------------------------------------------------------------
257	//
258	
259	// All tracks of the GPX file
260	var trks = gpxc._xmlDOM.documentElement.getElementsByTagName ("trk");
261	
262	if (trks.length > 0) {
263		var trk = trks[0];								// data object for the first track
264
265		// determine track name from <name>
266		var name = "";
267		if (typeof (trk.getElementsByTagName ("name")[0]) == "object") {
268			if (trks[0].getElementsByTagName ("name")[0].firstChild != null) {
269				name = trk.getElementsByTagName ("name")[0].firstChild.data;
270				name = gpxc.cleanTxt(name);
271			}
272		}
273		gpxc._trkName = name;
274
275		// Loop over track segments
276		//
277		var trkSegs = trk.getElementsByTagName ("trkseg");		// all track segments of the GPX file
278		var trksegs = new Array();		
279		for (var s = 0; s < trkSegs.length; s++) {
280
281			// Loop over track points
282			//
283			var trkPts = trkSegs[s].getElementsByTagName ("trkpt");		// all track points of the GPX file
284			var trkpts = new Array();		
285			for (var p = 0; p < trkPts.length; p++) {
286				lat = parseFloat (trkPts[p].getAttribute ("lat"));
287				lon = parseFloat (trkPts[p].getAttribute ("lon"));
288				ele = "--";
289				if (typeof (trkPts[p].getElementsByTagName ("ele")[0]) == "object") {
290					if (trkPts[p].getElementsByTagName ("ele")[0].firstChild != null) {
291						ele = parseFloat (trkPts[p].getElementsByTagName ("ele")[0].firstChild.data);
292						ele = +ele.toFixed (1);
293						if (isNaN (ele))
294							ele = "--";
295					}
296				}
297				time = "";
298				if (typeof (trkPts[p].getElementsByTagName ("time")[0]) == "object") {
299					if (trkPts[p].getElementsByTagName ("time")[0].firstChild != null) {
300						time = trkPts[p].getElementsByTagName ("time")[0].firstChild.data;
301					}
302				}
303				if (isNaN(lat) || isNaN(lon))	// invalid track point is skipped
304					continue;
305				
306				var trpt = new Array (4);		// track point: lat, lon, ele and time
307				trpt[0] = parseFloat(lat);
308				trpt[1] = parseFloat(lon);
309				trpt[2] = parseFloat(ele);
310				trpt[3] = time;
311				trkpts[p] = trpt;
312			}
313			trksegs[s] = trkpts;
314		}
315		gpxc._track = trksegs;
316	}
317}
318
319/**
320 * remove or replace invalid characters from name, cmt, or desc
321 * 
322 * @param	t	string to clean
323 * @return		cleaned string
324**/
325gpxc.cleanTxt = function(t) {
326	var s = t;
327	if (t.indexOf ("&") >= 0) {
328		if (t.indexOf ("&amp;") >= 0)
329			s = t.replace ("&amp;", "und");
330		// evtl. ergänzen
331		else if (t.indexOf ("&") >= 0)
332			s = t.replace ("&", "und");
333	}
334	return (s);
335}
336
337/**
338 * Smooth elevation profile 
339 *
340 * @param	smoothFactor		factor for strongness of smoothing
341 * @param	maxSpikeRatio		max ratio for elevation spike; derivation / length
342 *
343 * @globals	gpxc._track
344**/
345gpxc.smoothElevationData = function(smoothFactor, maxSpikeRatio) {
346	if (gpxc._track) {
347		var trk = gpxc._track;
348		var i, j, k, kh, dh1, dh2, dh, ratio, ratio1, f1, fa, fg, zs;
349		var lng1, lng2, lng;						// length of (p[i-1] - pi), (pi - p[i+1]), (p[i-1] - p[i+1])
350		var t1 = new Array ();						// for smoothed elevation data
351		
352		for (s = 0; s < trk.length; s++) {			// loop over segments
353			var trkseg = trk[s];
354			if (trkseg.length < 4)
355				return;								// track too short
356			
357			kh = "";
358			for (i = 0; i < trkseg.length;
358 i++) {	// search first valid elevation value
359				kh = trkseg[i][2];
360				if (!isNaN (kh))
361					break;
362			}
363			if (i == trkseg.length)					// no elevation values
364				return;
365			for (i = 0; i < trkseg.length; i++) {	// correct empty or invalid elevation values
366				if (isNaN(trkseg[i][2]))  
367					trkseg[i][2] = kh;
368				else                      
369					kh = trkseg[i][2];
370			}
371			//
372			// Elevation profile: adjust spikes greater than maxSpikeRatio
373			//
374			for (i = 1; i < (trkseg.length-1); i++) {
375				lng1 = gpxc.getLength ([trkseg[i-1][0], trkseg[i-1][1], trkseg[i][0], trkseg[i][1]]);
376				lng2 = gpxc.getLength ([trkseg[i][0], trkseg[i][1], trkseg[i+1][0], trkseg[i+1][1]]);
377				lng  = gpxc.getLength ([trkseg[i-1][0], trkseg[i-1][1], trkseg[i+1][0], trkseg[i+1][1]]);
378				
379				if (lng1 <= 0.01)
380					lng1 = 0.01;
381				if (lng2 <= 0.01)
382					lng2 = 0.01;
383				if (lng <= 0.01)
384					lng = 0.01;
385				dh1 = trkseg[i][2] - trkseg[i-1][2];
386				dh2 = trkseg[i+1][2] - trkseg[i][2];
387				ratio = Math.abs (dh1 - dh2) / lng;				// relative distance to the adjacent elevation values
388				ratio1 = Math.abs (dh1) / lng1;					// derivation from the predecessing elevation value
389				if (ratio > maxSpikeRatio && ratio1 > maxSpikeRatio) {
390					dh = trkseg[i+1][2] - trkseg[i-1][2];		// correct spikes
391					trkseg[i][2] = +(trkseg[i-1][2] + (dh * lng1 / (lng1 + lng2))).toFixed (1);
392				}
393			}
394
395			//
396			// Smooth elevation profile depending on the track length.
397			// The actual point and the adjacent points possibly are weighted stronger.
398			//
399			k = Math.ceil((Math.sqrt(trkseg.length) / 6) - 1);	// number of points taken for smothing
400			if (k < 1)  
401				k = 1;											//     to the left and to the right depending from track length
402			k = Math.ceil (k * smoothFactor);					// smoothing factor
403			fa = k + 2;   										// Höherbewertung des aktuellen Punktes     (3 ... )
404			f1 = +Math.sqrt (k).toFixed (1);					// Höherbewertung direkt benachbarter Punkte  (1.0 ... )
405			for (i = 0; i < trkseg.length; i++) {
406				zs = +(fa * trkseg[i][2]);						// aktueller Punkt, um "fa" stärker bewerten
407				fg = fa;
408				if (i >= 1) {
409					zs += +(f1 * trkseg[i-1][2]);				// Punkt davor, um "f1" stärker bewerten
410					fg += f1;
411				}
412				if (i < (trkseg.length-1)) {
413					zs += +(f1 * trkseg[i+1][2]);				// Punkt danach, um "f1" stärker bewerten
414					fg += f1;
415				}
416				for (j = 2; j <= k; ++j) {
417					if ((i-j) >= 0) {
418					zs += +(trkseg[i-j][2]);					// weitere Punkte davor
419					++fg;
420					}
421					if ((i+j) < trkseg.length) {
422						zs += +(trkseg[i+j][2]);				// weitere Punkte danach
423						++fg;
424					}
425				}
426				t1[i] = +(zs / fg).toFixed (1);
427			}
428			for (i = 0; i < trkseg.length; i++)
429				trkseg[i][2] = t1[i];							// copy new elevation value
430
431			trk[s] = trkseg;
432		}
433		gpxc._track = trk;
434	}
435}
436
437/**
438 * Reduce track points of the parsed track
439 *
440 * @param	minXyDeriv	minimum lateral derivation
441 * @param	minElDeriv	minimum elevation derivation
442 *
443 * @globals	gpxc._track
444**/
445gpxc.reducePoints = function(minXyDeriv, minElDeriv) {
446	if (gpxc._track) {
447		var trk = gpxc._track;
448		
449		for (s = 0; s < trk.length; s++) {			// loop over segments
450			var trkseg = trk[s];
451			var reducedList = new Array(0);
452
453			// shorten next track section with maximum air distance, continously
454			var pnt1 = 0;
455			var pnt2 = 1;
456			while (pnt2 < trkseg.length) {
457				// find point with maximum air distance
458				var dmax = 0;
459				for (var i = pnt2; i < trkseg.length; i++) {
460					d = gpxc.getLength([trkseg[pnt1][0], trkseg[pnt1][1], trkseg[i][0], trkseg[i][1]]);
461					if (d >= dmax) {
462						dmax = d;
463						pnt2 = i;
464					}
465				}
466				var tmp = reducedList.concat(gpxc.DouglasPeucker(trkseg, pnt1, pnt2, minXyDeriv, minElDeriv));
467				reducedList = tmp;
468				pnt1 = pnt2;
469				pnt2 = pnt1 + 1;
470			}
471			trk[s] = reducedList;
472		}
473		gpxc._track = trk;
474	}
475}
476
477/**
478 * Shorten trackpoints according to Douglas-Peucker algorithm
479 *
480 * @param	trkseg		track segment
481 * @param	pnt1		start of track section
482 * @param	pnt2		end of track section
483 * @param	minXyDeriv	minimum lateral derivation
484 * @param	minElDeriv	minimum elevation derivation
485 * @return				shortened track section
486**/
487
488gpxc.DouglasPeucker = function(trkseg, pnt1, pnt2, minXyDeriv, minElDeriv) {
489	var resultList = new Array(0);
490	if (pnt1 == pnt2)
491		return new Array(trkseg[pnt1]);
492	if (pnt1 + 1 == pnt2)
493		return new Array(trkseg[pnt1], trkseg[pnt2]);
494	
495	// find point with maximum lateral derivation between P1 and P2
496	var dmax = 0;
497	var pntm = pnt1 + 1;
498	var x1 = gpxc.getLength([trkseg[pnt1][0], trkseg[pnt1][1], trkseg[pnt1][0], 0]);
499	var y1 = gpxc.getLength([trkseg[pnt1][0], trkseg[pnt1][1], 0, trkseg[pnt1][1]]);
500	var x2 = gpxc.getLength([trkseg[pnt2][0], trkseg[pnt2][1], trkseg[pnt2][0], 0]);
501	var y2 = gpxc.getLength([trkseg[pnt2][0], trkseg[pnt2][1], 0, trkseg[pnt2][1]]);
502	var xd = x2 - x1;
503	var yd = y2 - y1;
504	if (xd != 0 || yd != 0) {
505		for (var i = pnt1 + 1; i < pnt2; i++) {
506			// perpendicular distance d
507			var x0 = gpxc.getLength([trkseg[i][0], trkseg[i][1], trkseg[i][0], 0]);
508			var y0 = gpxc.getLength([trkseg[i][0], trkseg[i][1], 0, trkseg[i][1]]);
509			var d = Math.abs(yd*x0 - xd*y0 + x2*y1 - y2*x1) / Math.sqrt(xd*xd + yd*yd);
510			if (d > dmax) {
511				pntm = i;
512				dmax = d;
513			}
514		}
515	}
516	// if maximum lateral derivation of a founded point is not greater than minXyDeriv 
517	// then determine its elevation derivation
518	var hm = trkseg[pntm][2];
519	var hms = 0;						// elevation of point concerned on slope (P1, P2) 
520	if (dmax <= minXyDeriv) {
521		var h1 = trkseg[pnt1][2];
522		var h2 = trkseg[pnt2][2];
523		var l12 = gpxc.getLength([trkseg[pnt1][0], trkseg[pnt1][1], trkseg[pnt2][0], trkseg[pnt2][1]]);
524		if (l12 > 0) {
525			if (h1 < h2)
526				hms = h1 + gpxc.getLength([trkseg[pnt1][0], trkseg[pnt1][1], trkseg[pntm][0], trkseg[pntm][1]]) / l12 * (h2 - h1);
527			else
528				hms = h2 + gpxc.getLength([trkseg[pntm][0], trkseg[pntm][1], trkseg[pnt2][0], trkseg[pnt2][1]]) / l12 * (h1 - h2);
529		}
530	}
531	// if derivations are greater than minXyDeriv rsp. minElDeriv then simplify recursively
532	if (dmax > minXyDeriv || Math.abs(hms - hm) > minElDeriv) {
533		// recursive calls
534		var recResults1 = gpxc.DouglasPeucker(trkseg, pnt1, pntm, minXyDeriv, minElDeriv);
535		var recResults2 = gpxc.DouglasPeucker(trkseg, pntm, pnt2, minXyDeriv, minElDeriv);
536
537		// build up result list
538		recResults1.splice(-1);
539		resultList = recResults1.concat(recResults2);
540	}
541	else	// delete points in between
542		resultList = new Array(trkseg[pnt1], trkseg[pnt2]);
543
544	return resultList;
545}
546
547/**
548 * Replace elevation data using Open-Elevation Online Service
549 * 
550 * The service only allows POST sizes of about 2100 track points. Therefore the request for elevation data
551 * is done in packages. It may be that after the first requests are sent the process hangs.
552 *
553 * @return	status code
554 * @globals	gpxc._track
555**/
556gpxc.replaceElevationData = function() {
557	var status = 0;
558	var p = 2100;		// package size (#track points), maximum for open-elevation
559	if (gpxc._track) {
560		var trk = gpxc._track;
561		for (s = 0; s < trk.length; s++) {			// loop over segments
562			trkseg = trk[s];
563			for (var k = 0; k <= Math.floor((trkseg.length-1)/p); k++) {	// in packages of p tracks
564				var h = k*p;
565				var latLngCollection = "";
566				for (var j = 0; j < p; j++) {
567					if (h+j >= trkseg.length)
568						break;
569					if (j == 0)
570						latLngCollection = "{\"locations\": [{\"latitude\": " + trkseg[h+j][0] + ", \"longitude\": " + trkseg[h+j][1] + "}";
571					else
572						latLngCollection +=				  ", {\"latitude\": " + trkseg[h+j][0] + ", \"longitude\": " + trkseg[h+j][1] + "}";
573				}
574				latLngCollection += "]}";
575				
576				var result;
577				if (result = gpxc.getElevationData(latLngCollection)) {
578					for (var j = 0; j < p; j++) {
579						if (h+j >= trkseg.length)
580							break;
581						trkseg[h+j][2] = result.results[j].elevation;
582					}
583				}
584				else
585					status = 600;
586			}
587			trk[s] = trkseg;
588		}
589		gpxc._track = trk;
590		return status;
591	}
592	else
593		return 600;
594}
595
596/**
597 * Get elevation data from Open-Elevation Online Service
598 *
599 * @param	latLngCollection	List of coordinates for which elevation data are requested
600 * @return	result				of Open-Elevation service
601 *
602**/
603gpxc.getElevationData = function(latLngCollection) {
604	var result = false;
605	var url = "https://api.open-elevation.com/api/v1/lookup";
606	var xhr = new XMLHttpRequest();
607	xhr.open("POST", url, false);				// synchronous request
608	xhr.setRequestHeader('Accept', 'application/json');
609	xhr.setRequestHeader('Content-Type', 'application/json');
610
611	xhr.onreadystatechange = function () {
612		if (this.readyState != 4)
613			return;
614		if (this.status == 200)
615			result = JSON.parse(this.responseText);
616	};
617	xhr.onerror = function () {					// continues here when request fails
618		alert(clean_translation.bad_request);
619		this.abort();
620	}
621	xhr.send(latLngCollection);					// waits until request is done
622	if (result.error) {
623		alert(result.error);
624		return false;
625	}
626	return result;
627}
628
629/**
630 * Complete edited track with elevation and time data
631 * 
632 * @globals	gpxc._track
633**/
634
635gpxc.completeTrack = function() {
636	var trk = gpxc._track;
637	var trk_no = 0;
638	var polylines  = gpxm.map.getEditablePolylines();
639	var new_track  = new Array();
640
641	// get elevations for all single point tracks
642	var w = 0;
643	var latLngCollection = "{\"locations\": ";
644	for (var s = 0; s < polylines.length; s++) {
645		var polyline = polylines[s];
646		var points = polyline.getPoints();
647
648		if (points.length == 1)	{
649			if (w++ == 0)
650				latLngCollection += "[{\"latitude\": " + points[0].getLatLng().lat + ", \"longitude\": " + points[0].getLatLng().lng + "}";
651			else
652				latLngCollection +=	", {\"latitude\": " + points[0].getLatLng().lat + ", \"longitude\": " + points[0].getLatLng().lng + "}";
653		}
654	}
655	latLngCollection += "]}";
656	if (w > 0)
657		var elevations = gpxc.getElevationData(latLngCollection);
658
659	var t = 0;
660	w = 0;
661	for (var s = 0; s < polylines.length; s++) {
662		var polyline = polylines[s];
663		var points = polyline.getPoints();
664		var new_seg = Array();
665
666		if (points.length == 1) {			// treat single point track as way point
667			var new_wpt = new Array (7);	// way point: lat, lon, ele, name, cmt, desc, sym
668			new_wpt[0] = points[0].getLatLng().lat;
669			new_wpt[1] = points[0].getLatLng().lng;
670			new_wpt[2] = elevations.results[w].elevation;
671			new_wpt[3] = '';
672			new_wpt[4] = 'Name-' + ++w;
673			new_wpt[5] = '';
674			new_wpt[6] = '';
675			new_wpt[7] = '';
676			gpxc._wpts.push(new_wpt);
677
678			continue;
679		}
680		else if (trk_no > 0)
681			continue;
682		for (var p = 0; p < points.length; p++) {
683			var new_pnt = new Array (4);			// track point: lat, lon, ele and time
684			new_pnt[0] = points[p].getLatLng().lat;
685			new_pnt[1] = points[p].getLatLng().lng;
686			var q = null; 
687			var r = null;
688			if (points[p].context) {
689				r = points[p].context.originalPolylineNo;
690				q = points[p].context.originalPointNo;
691			}
692			if (trk != null && q !== null && r !== null) {	// take original ele and time data
693				new_pnt[2] = trk[r][q][2];
694				new_pnt[3] = trk[r][q][3];
695			}
696			else {				// take ele and time data of previous point, if possible
697				if (p == 0) {
698					new_pnt[2] = '';
699					var latLng = JSON.stringify({"locations": [{"latitude": new_pnt[0], "longitude": new_pnt[1]}]});
700					var result;
701					if (result = gpxc.getElevationData(latLng))
702						new_pnt[2] = result.results[0].elevation;
703					new_pnt[3] = '';
704				}
705				else {
706					new_pnt[2] = new_seg[p-1][2];
707					new_pnt[3] = new_seg[p-1][3];
708				}
709			}
710			new_seg[p] = new_pnt;
711		}
712		if (new_seg.length > 0)
713			new_track[t++] = new_seg;
714		trk_no++;
715	}
716	gpxc._track = new_track;
717}
718
719/**
720 * Create a new content for the GPX file
721 * 
722 * @param label		name of the track
723 * @param cleaned	t/f: track was cleaned
724 * @param replaced	t/f: elevation data were replaced
725 * @return			content of GPX file
726**/
727gpxc.newTrack = function(label, cleaned, replaced) {
728	var ele, nam, cmt, desc, desc1, sym, time;
729	var wpts = gpxc._wpts;
730	if (!gpxc._attributes)
731		gpxc._attributes =	'version="1.1" creator="WP, GPX Viewer"'
732						+	' xmlns="http://www.topografix.com/GPX/1/1"'
733						+	' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
734						+	' xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"'
735						+	' xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"'
736						+	' xmlns:gpxtrkx="http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"'
737						+	' xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v2"'
738						+	' xmlns:locus="http://www.locusmap.eu"';
739	//
740	// ---------- GPX Header -----------------------------------------------------
741	//
742	var gpxText = '<?xml version="1.0" encoding="utf-8"?>\n'
743				+ '<gpx ' + gpxc._attributes + '>\n\n';
744
745	//
746	// ---------- Way points ------------------------------------------------------
747	//
748	var wpt = new Array (7);
749	for (w = 0; w < wpts.length; w++) {
750		wpt = wpts[w];
751		gpxText 			+= '	<wpt lat="' + wpt[0] + '" lon="' + wpt[1] + '">\n';
752		ele = wpt[2];
753		if (ele == null || ele == "undefined")    
754			ele = "--";
755		if (! isNaN (ele))    
756			gpxText 		+= '		<ele>' + ele + '</ele>\n';
757		tim = wpt[3];
758		if (tim != "")       
759			gpxText 		+= '		<time>' + tim + '</time>\n';
760		nam = wpt[4];
761		if (nam != "")       
762			gpxText 		+= '		<name>' + nam + '</name>\n';
763		cmt = wpt[5];
764		if (cmt != "")        
765			gpxText 		+= '		<cmt>' + cmt + '</cmt>\n';
766		desc = wpt[6];
767		if (desc != "")       
768			gpxText 		+= ' 		<desc>' + desc + '</desc>\n';
769		sym = wpt[7];
770		if (sym != "")        
771			gpxText 		+= '		<sym>' + sym + '</sym>\n';
772		if (gpxc._wpt_extension != "")        
773			gpxText 		+= '		<extensions>' + gpxc._wpt_extension + '\t</extensions>\n';
774		gpxText 			+= '	</wpt>\n';
775	}
776	//
777	// ---------- Tracks ---------------------------------------------------------
778	//
779	if (gpxc._track) {
780		trk = gpxc._track;
781		var dist	= gpxc.totalDistance(trk, true);
782		var hm		= gpxc.calcAscentDescent(trk, true);
783		var rauf	= (hm.length == 2)? hm[0] : "--";
784		var runter	= (hm.length == 2)? hm[1] : "--";
785		var noEle	= (hm.length == 0)? true : false;
786		label = (label == "")? gpxc._trkName : label;
787		gpxText 			+= '	<trk>\n';
788		gpxText 			+= '		<name>' + label + '</name>\n';
789		gpxText 			+= '		<cmt> distance: ' + dist + ' km, up: ' + rauf + ' m, down: ' + runter + ' m</cmt>\n';
790		desc = "";
791		if (!cleaned && !replaced)
792			desc = "original data of track points";
793		if (cleaned && !replaced)
794			desc = "track points cleaned";
795		if (replaced) {
796			desc = "elevation data replaced";
797			if (cleaned)
798				desc += " and smoothed";
799		}
800		if (noEle)
801			desc += ", no elevation data";
802		gpxText 			+= '		<desc>' + trk.length + ' track segments, ' + desc + '</desc>\n';
803		if (gpxc._trk_extension != "")        
804			gpxText 		+= '		<extensions>' + gpxc._trk_extension + '\t</extensions>\n';
805		for (s = 0; s < trk.length; s++) {
806			trkpnts = trk[s];
807			if (Array.isArray(trkpnts)) {
808				gpxText 	+= '		<trkseg>\n';
809				for (p = 0; p < trkpnts.length; p++) {
810					trpt = trkpnts[p];
811					gpxText += '			<trkpt lat="' + trpt[0] + '" lon="' + trpt[1] + '">\n';
812					ele	 = trpt[2];
813					time = trpt[3];
814					if (! isNaN(ele) && ele != "")      
815						gpxText	
816							+= '				<ele>' + trpt[2] + '</ele>\n';
817					if (time != "")         
818						gpxText	
819							+= '				<time>' + trpt[3] + '</time>\n';
820					gpxText += '			</trkpt>\n';
821				}
822				gpxText 	+= '		</trkseg>\n';
823			}
824		}
825		gpxText 			+= '	</trk>\n';
826	}
827	//
828	// ---------- GPX Trailer
829	//
830	gpxText += '</gpx>\n';
831	
832	return gpxText;
833}
834
835
836/**
837 * Calculate distance between two points in meters
838 * 
839 * The distance between circles of latitudes is always the same:
840 * 		circumference of the earth  / 360° = 40075017 m  / 360 = 111300 m
841 * The distance between circles of longitudes variies depending on the latitude. 
842 * At the equator it is also 111300 m, meanwhile at the poles it is zero.
843 * Thus the distance is 111300 m * cos(lat).
844 * 
845 * @param	line	between two points [lat1, lon1, lat2, lon2]
846 * @return			distance in meters
847**/
848
849gpxc.getLength = function(line) {
850	var lat, lng, deltaLat, deltaLon;
851	deltaLat = gpxc._meterJeGradLat * (line[0] - line[2]);		// [m]
852	lat = (line[0] + line[2]) / 2;      						// mean value
853	deltaLon = gpxc._meterJeGradLon (lat) * (line[1] - line[3]);// [m]
854	if (deltaLat == 0)      
855		lng = deltaLon;
856	else if (deltaLon == 0)       
857		lng = deltaLat;
858	else if (deltaLat == 0 && deltaLon == 0) 
859		lng = 0;
860	else                                 
861		lng = Math.sqrt ((deltaLat * deltaLat) + (deltaLon * deltaLon));
862	if (isNaN (lng))                     
863		lng = 0;
864	return (lng);												// length [m]
865}
866
867
868/**
869 * Calculate total distance of a track in kilometers
870 * 
871 * The distance between circles of latitudes is always the same:
872 * 		circumference of the earth  / 360° = 40075017 m  / 360 = 111300 m
873 * The distance between circles of longitudes variies depending on the latitude. 
874 * At the equator it is also 111300 m, meanwhile at the poles it is zero.
875 * Thus the distance is 111300 m * cos(lat).
876 * 
877 * @param	trk		track data
878 * @param	rd		true/false: with/without rounding
879 * @return			distance in kilometers
880**/
881
882gpxc.totalDistance = function(trk, rd) {
883	var lat, lng, deltaLat, deltaLon, deltaEle;
884	var dist = 0;     								// track distance [m]
885	
886	for (s = 0; s < trk.length; s++) {				// loop over segments
887		var trkpnts = trk[s];
888		if (Array.isArray(trkpnts)) {
889			for (var i = 0; i < trkpnts.length - 1; i++) {
890				deltaLat = gpxc._meterJeGradLat * (trkpnts[i][0] - trkpnts[i+1][0]);		// [m]
891				lat = Math.abs ((trkpnts[i][0] + trkpnts[i+1][0]) / 2);	// Mittelwert
892				deltaLon = gpxc._meterJeGradLon (lat) * (trkpnts[i][1] - trkpnts[i+1][1]);	// [m]
893				deltaEle = trkpnts[i][2] - trkpnts[i+1][2];     							// [m]
894				if (isNaN(deltaEle))    
895					deltaEle = 0;
896				if (deltaLat == 0)            
897					lng = deltaLon;
898				else if (deltaLon == 0)         
899					lng = deltaLat;
900				else if (deltaLat == 0 && deltaLon == 0)  
901					lng = 0;
902				else                                    
903					lng = Math.sqrt ((deltaLat * deltaLat) + (deltaLon * deltaLon));
904				if (isNaN(lng))                         
905					lng = 0;
906				lng = Math.sqrt (lng*lng + deltaEle*deltaEle);
907				dist += lng;						// [m]
908			}
909		}
910	}
911	dist = dist / 1000;								// [km]
912	if (!rd)                       
913		dist = +dist.toFixed (3);
914	else if (dist < 20)             
915		dist = +dist.toFixed (1);
916	else                            
917		dist = Math.floor (dist);
918	return (dist);
919}
920
921/**
922 * Calculate total ascent resp. descent in meters
923 * 
924 * @param	trk		track data
925 * @param	rd		true/false: with/without rounding
926 * @return			total ascent/descent in meters or empty array
927**/
928
929gpxc.calcAscentDescent = function(trk, rd) {
930	var d;
931	var alt = new Array (0);
932	var rauf = 0;
933	var runter = 0;
934	for (s = 0; s < trk.length; s++) {				// loop over segments
935		var trkpnts = trk[s];
936		if (Array.isArray(trkpnts)) {
937			if (isNaN(trkpnts[0][2]))
938				return (alt);
939			for (var p = 1; p < trkpnts.length; p++) {
940				if (isNaN(trkpnts[p][2]))
941					return (alt);
942				var h = trkpnts[p][2] - trkpnts[p-1][2];
943				if (h >= 0)
944					rauf += h;
945				else
946					runter -= h;
947			}
948		}
949	}
950	if (rd) {
951		alt[0] = Math.round (rauf);
952		alt[1] = Math.round (runter);
953	} else {
954		alt[0] = rauf;
955		alt[1] = runter;
956	}
957	return (alt);
958}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.