1/** 2 * Corrections for GPX-Viewer 3 * - url to icons 4 * - name of track (not of first waypoint) 5 * 6 * Enhancements 7 * - Editable track points 8 * - Context information of waypoints extended by duration from start 9 * 10* Copyright (C) 2011-2012 Pavel Shramov 11* Copyright (C) 2013-2017 Maxime Petazzoni <[email protected]> 12* All Rights Reserved. 13* 14* Redistribution and use in source and binary forms, with or without 15* modification, are permitted provided that the following conditions are met: 16* 17* - Redistributions of source code must retain the above copyright notice, 18* this list of conditions and the following disclaimer. 19* 20* - Redistributions in binary form must reproduce the above copyright notice, 21* this list of conditions and the following disclaimer in the documentation 22* and/or other materials provided with the distribution. 23* 24* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 28* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 29* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 31* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 32* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34* POSSIBILITY OF SUCH DAMAGE. 35*/ 36 37/* 38* Thanks to Pavel Shramov who provided the initial implementation and Leaflet 39* integration. Original code was at https://github.com/shramov/leaflet-plugins. 40* 41* It was then cleaned-up and modified to record and make available more 42* information about the GPX track while it is being parsed so that the result 43* can be used to display additional information about the track that is 44* rendered on the Leaflet map. 45*/ 46 47var L = L || require('leaflet'); 48 49var _MAX_POINT_INTERVAL_MS = 15000; 50var _SECOND_IN_MILLIS = 1000; 51var _MINUTE_IN_MILLIS = 60 * _SECOND_IN_MILLIS; 52var _HOUR_IN_MILLIS = 60 * _MINUTE_IN_MILLIS; 53var _DAY_IN_MILLIS = 24 * _HOUR_IN_MILLIS; 54 55L.GPX = L.FeatureGroup.extend({ 56 initialize: function(editable, options) { 57 options.max_point_interval = _MAX_POINT_INTERVAL_MS; 58 59 L.Util.setOptions(this, options); 60 61 // Base icon class for track pins. 62 L.GPXTrackIcon = L.Icon.extend({ options: options.marker_options }); 63 64 this._editable = editable; 65 this.options = options; 66 this._layers = {}; 67 this._init_info(); 68 }, 69 70 // Public methods 71 72 load_track: function(gpx) { 73 if (gpx != '' && gpx != 'newtrack') 74 this._parse(gpx, this.options, this.options.async); 75 else if (gpx == 'newtrack' && this._editable) { 76 var l = new L.Polyline.PolylineEditor(new Array(0), this.options.polyline_options); 77 this.fire('addline', { line: l }); 78 this.addLayer(l); 79 this.options.isLoaded = true; 80 } 81 }, 82 83 get_duration_string: function(duration, hide_secs) { 84 var s = ''; 85 86 if (duration >= _DAY_IN_MILLIS) { 87 s += Math.floor(duration / _DAY_IN_MILLIS) + ' d '; 88 duration = duration % _DAY_IN_MILLIS; 89 } 90 91 if (duration >= _HOUR_IN_MILLIS) { 92 s += Math.floor(duration / _HOUR_IN_MILLIS) + ':'; 93 duration = duration % _HOUR_IN_MILLIS; 94 } 95 else 96 s += '00:'; 97 98 var mins = Math.floor(duration / _MINUTE_IN_MILLIS); 99 duration = duration % _MINUTE_IN_MILLIS; 100 if (mins < 10) s += '0'; 101 s += mins + ' h '; 102 103 if (!hide_secs) { 104 var secs = Math.floor(duration / _SECOND_IN_MILLIS); 105 duration = duration % _SECOND_IN_MILLIS; 106 if (secs < 10) s += '0'; 107 s += secs + ' s'; 108 } 109 return s; 110 }, 111 112 get_duration_string_iso: function(duration, hidems) { 113 var s = this.get_duration_string(duration, hidems); 114 return s.replace("'",':').replace('"',''); 115 }, 116 117 to_miles: function(v) { return v / 1.60934; }, 118 to_ft: function(v) { return v * 3.28084; }, 119 m_to_km: function(v) { return v / 1000; }, 120 m_to_mi: function(v) { return v / 1609.34; }, 121 122 get_name: function() { return this._info.name; }, 123 get_desc: function() { return this._info.desc; }, 124 get_author: function() { return this._info.author; }, 125 get_copyright: function() { return this._info.copyright; }, 126 get_distance: function() { return this._info.length; }, 127 get_distance_imp: function() { return this.to_miles(this.m_to_km(this.get_distance())); }, 128 129 get_start_time: function() { return this._info.duration.start; }, 130 get_end_time: function() { return this._info.duration.end; }, 131 get_moving_time: function() { return this._info.duration.moving; }, 132 get_total_time: function() { return this._info.duration.total; }, 133 134 get_moving_pace: function() { return this.get_moving_time() / this.m_to_km(this.get_distance()); }, 135 get_moving_pace_imp: function() { return this.get_moving_time() / this.get_distance_imp(); }, 136 137 get_moving_speed: function() { return this.m_to_km(this.get_distance()) / (this.get_moving_time() / (3600 * 1000)) ; }, 138 get_moving_speed_imp:function() { return this.to_miles(this.m_to_km(this.get_distance())) / (this.get_moving_time() / (3600 * 1000)) ; }, 139 140 get_total_speed: function() { return this.m_to_km(this.get_distance()) / (this.get_total_time() / (3600 * 1000)); }, 141 get_total_speed_imp: function() { return this.to_miles(this.m_to_km(this.get_distance())) / (this.get_total_time() / (3600 * 1000)); }, 142 143 get_elevation_gain: function() { return this._info.elevation.gain; }, 144 get_elevation_loss: function() { return this._info.elevation.loss; }, 145 get_elevation_gain_imp: function() { return this.to_ft(this.get_elevation_gain()); }, 146 get_elevation_loss_imp: function() { return this.to_ft(this.get_elevation_loss()); }, 147 get_elevation_data: function() { 148 var _this = this; 149 return this._info.elevation._points.map( 150 function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, 151 function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' m'; }); 152 }); 153 }, 154 get_elevation_data_imp: function() { 155 var _this = this; 156 return this._info.elevation._points.map( 157 function(p) { return _this._prepare_data_point(p, _this.m_to_mi, _this.to_ft, 158 function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' ft'; }); 159 }); 160 }, 161 get_elevation_max: function() { return this._info.elevation.max; }, 162 get_elevation_min: function() { return this._info.elevation.min; }, 163 get_elevation_max_imp: function() { return this.to_ft(this.get_elevation_max()); }, 164 get_elevation_min_imp: function() { return this.to_ft(this.get_elevation_min()); }, 165 166 get_average_hr: function() { return this._info.hr.avg; }, 167 get_average_temp: function() { return this._info.atemp.avg; },
168 get_average_cadence: function() { return this._info.cad.avg; }, 169 get_heartrate_data: function() { 170 var _this = this; 171 return this._info.hr._points.map( 172 function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, 173 function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' bpm'; }); 174 }); 175 }, 176 get_heartrate_data_imp: function() { 177 var _this = this; 178 return this._info.hr._points.map( 179 function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, 180 function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' bpm'; }); 181 }); 182 }, 183 get_cadence_data: function() { 184 var _this = this; 185 return this._info.cad._points.map( 186 function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, 187 function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' rpm'; }); 188 }); 189 }, 190 get_temp_data: function() { 191 var _this = this; 192 return this._info.atemp._points.map( 193 function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, 194 function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' degrees'; }); 195 }); 196 }, 197 get_cadence_data_imp: function() { 198 var _this = this; 199 return this._info.cad._points.map( 200 function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, 201 function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' rpm'; }); 202 }); 203 }, 204 get_temp_data_imp: function() { 205 var _this = this; 206 return this._info.atemp._points.map( 207 function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, 208 function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' degrees'; }); 209 }); 210 }, 211 212 // Private methods 213 _merge_objs: function(a, b) { 214 var _ = {}; 215 for (var attr in a) { _[attr] = a[attr]; } 216 for (var attr in b) { _[attr] = b[attr]; } 217 return _; 218 }, 219 220 _prepare_data_point: function(p, trans1, trans2, trans_tooltip) { 221 var r = [trans1 && trans1(p[0]) || p[0], trans2 && trans2(p[1]) || p[1]]; 222 r.push(trans_tooltip && trans_tooltip(r[0], r[1]) || (r[0] + ': ' + r[1])); 223 return r; 224 }, 225 226 _init_info: function() { 227 this._info = { 228 name: null, 229 length: 0.0, 230 elevation: {gain: 0.0, loss: 0.0, max: 0.0, min: Infinity, _points: []}, 231 hr: {avg: 0, _total: 0, _points: []}, 232 duration: {start: null, end: null, moving: 0, total: 0}, 233 atemp: {avg: 0, _total: 0, _points: []}, 234 cad: {avg: 0, _total: 0, _points: []} 235 }; 236 }, 237 238 _load_xml: function(url, cb, options, async) { 239 if (async == undefined) async = this.options.async; 240 if (options == undefined) options = this.options; 241 242 var req = new window.XMLHttpRequest(); 243 req.open('GET', url, async); 244 try { 245 req.overrideMimeType('text/xml'); // unsupported by IE 246 } catch(e) {} 247 req.onreadystatechange = function() { 248 if (req.readyState != 4) return; 249 if(req.status == 200) cb(req.responseXML, options); 250 }; 251 req.send(null); 252 }, 253 254 _parse: function(input, options, async) { 255 var _this = this; 256 var cb = function(gpx, options) { 257 var layers = _this._parse_gpx_data(gpx, options); 258 if (!layers) 259 return; 260 _this.addLayer(layers); 261 _this.fire('loaded'); 262 } 263 if (input.substr(0,1) === '<') { // direct XML has to start with a < 264 var parser = new DOMParser(); 265 if (async) { 266 setTimeout(function() { 267 cb(parser.parseFromString(input, "text/xml"), options); 268 }); 269 } else 270 cb(parser.parseFromString(input, "text/xml"), options); 271 } else 272 this._load_xml(input, cb, options, async); 273 }, 274 275 _parse_gpx_data: function(xml, options) { 276 var j, i, el, layers = []; 277 var tags = []; 278 279 var parseElements = options.gpx_options.parseElements; 280 if (parseElements.indexOf('route') > -1) 281 tags.push(['rte','rtept']); 282 if (parseElements.indexOf('track') > -1) 283 tags.push(['trkseg','trkpt']); 284 var trks = xml.getElementsByTagName('trk'); 285 if (trks.length > 0) { 286 var names = trks[0].getElementsByTagName('name'); 287 if (names.length > 0) 288 this._info.name = names[0].textContent; 289 } 290 var desc = xml.getElementsByTagName('desc'); 291 if (desc.length > 0) 292 this._info.desc = desc[0].textContent; 293 var author = xml.getElementsByTagName('author'); 294 if (author.length > 0) 295 this._info.author = author[0].textContent; 296 var copyright = xml.getElementsByTagName('copyright'); 297 if (copyright.length > 0) 298 this._info.copyright = copyright[0].textContent; 299 300 var all_coords = new Array(tags.length); 301 for (j = 0; j < tags.length; j++) { 302 el = xml.getElementsByTagName(tags[j][0]); 303 all_coords[j] = new Array(el.length); 304 for (i = 0; i < el.length; i++) { 305 var coords = this._parse_trkseg(el[i], xml, options, tags[j][1]); 306 if (coords.length === 0) 307 continue;
308 all_coords[j][i] = coords; 309 310 // add track 311 if (this._editable) 312 var l = new L.Polyline.PolylineEditor(coords, options.polyline_options); 313 else 314 var l = new L.Polyline(coords, options.polyline_options); 315 this.fire('addline', { line: l }); 316 layers.push(l); 317 318 if (options.marker_options.startIcon || options.marker_options.startIconUrl) { 319 // add start pin 320 var p = new L.Marker(coords[0], { 321 icon: options.marker_options.startIcon || new L.GPXTrackIcon({iconUrl: options.marker_options.startIconUrl}), 322 zIndexOffset: 5 323 }); 324 this.fire('addpoint', { point: p, point_type: 'start' }); 325 layers.push(p); 326 } 327 if (options.marker_options.endIcon || options.marker_options.endIconUrl) { 328 // add end pin 329 p = new L.Marker(coords[coords.length-1], { 330 icon: options.marker_options.endIcon || new L.GPXTrackIcon({iconUrl: options.marker_options.endIconUrl}) 331 }); 332 this.fire('addpoint', { point: p, point_type: 'end' }); 333 layers.push(p); 334 } 335 } 336 } 337 this._info.hr.avg = Math.round(this._info.hr._total / this._info.hr._points.length); 338 this._info.cad.avg = Math.round(this._info.cad._total / this._info.cad._points.length); 339 this._info.atemp.avg = Math.round(this._info.atemp._total / this._info.atemp._points.length); 340 341 // parse waypoints and add markers for each of them 342 if (parseElements.indexOf('waypoint') > -1) { 343 el = xml.getElementsByTagName('wpt'); 344 for (i = 0; i < el.length; i++) { 345 var ll = new L.LatLng( 346 el[i].getAttribute('lat'), 347 el[i].getAttribute('lon')); 348 var nameEl = el[i].getElementsByTagName('name'); 349 var name = ''; 350 if (nameEl.length > 0) 351 name = nameEl[0].textContent; 352 var timeEl = el[i].getElementsByTagName('time'); 353 var time = ''; 354 if (timeEl.length > 0) 355 time = new Date(Date.parse(timeEl[0].textContent)); 356 else { 357 var min_dist = 6371000; 358 for (j = 0; j < all_coords.length; j++) { 359 for (k = 0; k < all_coords[j].length; k++) { 360 var coords = all_coords[j][k]; 361 for(c = 0; c < coords.length; c++) { 362 var dist = this._dist2d(ll, coords[c]); 363 if (dist < min_dist) { 364 min_dist = dist; 365 time = coords[c].meta.time; 366 } 367 } 368 } 369 } 370 if (min_dist > 25) 371 time = ''; 372 } 373 if (time != '') { 374 var duration = Math.abs(time - this._info.duration.start); 375 if (duration > 0) 376 name += '\n' + this.get_duration_string(duration, true); 377 } 378 var descEl = el[i].getElementsByTagName('desc'); 379 var desc = ''; 380 if (descEl.length > 0) 381 desc = descEl[0].textContent; 382 var symEl = el[i].getElementsByTagName('sym'); 383 var symKey = ''; 384 if (symEl.length > 0) 385 symKey = symEl[0].textContent; 386 387 /* 388 * Add waypoint marker based on the waypoint symbol key. 389 * 390 * First look for a configured icon for that symKey. If not found, look 391 * for a configured icon URL for that symKey and build an icon from it. 392 * Otherwise, fall back to the default icon if one was configured, or 393 * finally to the default icon URL. 394 */ 395 var wptIcons = options.marker_options.wptIcons; 396 var wptIconUrls = options.marker_options.wptIconUrls; 397 var symIcon; 398 if (wptIcons && wptIcons[symKey]) 399 symIcon = wptIcons[symKey]; 400 else if (wptIconUrls && wptIconUrls[symKey]) 401 symIcon = new L.GPXTrackIcon({iconUrl: wptIconUrls[symKey]}); 402 else if (wptIcons && wptIcons['']) 403 symIcon = wptIcons['']; 404 else if (wptIconUrls && wptIconUrls['']) 405 symIcon = new L.GPXTrackIcon({iconUrl: wptIconUrls['']}); 406 else { 407 console.log('No icon or icon URL configured for symbol type "' + symKey 408 + '", and no fallback configured; ignoring waypoint.'); 409 continue; 410 } 411 var marker = new L.Marker(ll, { 412 clickable: true, 413 title: name, 414 icon: symIcon 415 }); 416 marker.bindPopup("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : '')).openPopup(); 417 this.fire('addpoint', { point: marker, point_type: 'waypoint' }); 418 layers.push(marker); 419 } 420 } 421 422 if (layers.length > 1) 423 return new L.FeatureGroup(layers); 424 else if (layers.length == 1) 425 return layers[0]; 426 }, 427 428 _parse_trkseg: function(line, xml, options, tag) { 429 var el = line.getElementsByTagName(tag); 430 if (!el.length) return []; 431 var coords = []; 432 var last = null; 433 var last_time = null; 434 435 for (var i = 0; i < el.length; i++) { 436 var _, ll = new L.LatLng( 437 el[i].getAttribute('lat'), 438 el[i].getAttribute('lon')); 439 ll.meta = { time: null, ele: null, hr: null, cad: null, atemp: null }; 440 441 _ = el[i].getElementsByTagName('time'); 442 if (_.length > 0) 443 ll.meta.time = new Date(Date.parse(_[0].textContent)); 444 else 445 ll.meta.time = new Date('1970-01-01T00:00:00'); 446 447 _ = el[i].getElementsByTagName('ele'); 448 if (_.length > 0) 449 ll.meta.ele = parseFloat(_[0].textContent); 450 451 _ = el[i].getElementsByTagNameNS('*', 'hr'); 452 if (_.length > 0) { 453 ll.meta.hr = parseInt(_[0].textContent); 454 this._info.hr._points.push([this._info.length, ll.meta.hr]); 455 this._info.hr._total += ll.meta.hr; 456 } 457 _ = el[i].getElementsByTagNameNS('*', 'cad'); 458 if (_.length > 0) { 459 ll.meta.cad = parseInt(_[0].textContent); 460 this._info.cad._points.push([this._info.length, ll.meta.cad]); 461 this._info.cad._total += ll.meta.cad; 462 } 463 _ = el[i].getElementsByTagNameNS('*', 'atemp'); 464 if (_.length > 0) {
465 ll.meta.atemp = parseInt(_[0].textContent); 466 this._info.atemp._points.push([this._info.length, ll.meta.atemp]); 467 this._info.atemp._total += ll.meta.atemp; 468 } 469 if (ll.meta.ele > this._info.elevation.max) 470 this._info.elevation.max = ll.meta.ele; 471 472 if (ll.meta.ele < this._info.elevation.min) 473 this._info.elevation.min = ll.meta.ele; 474 475 this._info.elevation._points.push([this._info.length, ll.meta.ele]); 476 this._info.duration.end = ll.meta.time; 477 478 if (last != null) { 479 this._info.length += this._dist3d(last, ll); 480 481 var t = ll.meta.ele - last.meta.ele; 482 if (t > 0) 483 this._info.elevation.gain += t; 484 else 485 this._info.elevation.loss += Math.abs(t); 486 } else 487 this._info.duration.start = ll.meta.time; 488 489 490 if (ll.meta.time.getTime() > 0) { 491 if (last_time != null) { 492 t = Math.abs(ll.meta.time - last_time); 493 this._info.duration.total += t; 494 if (t < options.max_point_interval) 495 this._info.duration.moving += t; 496 } 497 last_time = ll.meta.time; 498 } 499 500 last = ll; 501 coords.push(ll); 502 } 503 return coords; 504 }, 505 506 _dist2d: function(a, b) { 507 var R = 6371000; 508 var dLat = this._deg2rad(b.lat - a.lat); 509 var dLon = this._deg2rad(b.lng - a.lng); 510 var r = Math.sin(dLat/2) * 511 Math.sin(dLat/2) + 512 Math.cos(this._deg2rad(a.lat)) * 513 Math.cos(this._deg2rad(b.lat)) * 514 Math.sin(dLon/2) * 515 Math.sin(dLon/2); 516 var c = 2 * Math.atan2(Math.sqrt(r), Math.sqrt(1-r)); 517 var d = R * c; 518 return d; 519 }, 520 521 _dist3d: function(a, b) { 522 var planar = this._dist2d(a, b); 523 var height = Math.abs(b.meta.ele - a.meta.ele); 524 return Math.sqrt(Math.pow(planar, 2) + Math.pow(height, 2)); 525 }, 526 527 _deg2rad: function(deg) { 528 return deg * Math.PI / 180; 529 } 530}); 531 532if (typeof module === 'object' && typeof module.exports === 'object') 533 module.exports = L; 534else if (typeof define === 'function' && define.amd) 535 define(L); 536
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.