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 ("&") >= 0) 329 s = t.replace ("&", "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.