├── .gitignore ├── LICENSE ├── README.md ├── gpx.js ├── icons ├── pin-icon-end.png ├── pin-icon-start.png ├── pin-icon-wpt.png └── pin-shadow.png └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *~ 3 | *.swp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011-2012 Pavel Shramov 2 | Copyright (C) 2013 Maxime Petazzoni 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | - Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | - Redistributions in binary form must reproduce the above copyright notice, this 12 | list of conditions and the following disclaimer in the documentation and/or 13 | other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPX plugin for Leaflet 2 | 3 | [![CDNJS](https://img.shields.io/cdnjs/v/leaflet-gpx.svg)](https://cdnjs.com/libraries/leaflet-gpx) 4 | 5 | [Leaflet](http://www.leafletjs.com) is a Javascript library for displaying 6 | interactive maps. This plugin, based on the work of [Pavel 7 | Shramov](http://github.com/shramov) and his 8 | [leaflet-plugins](http://github.com/shramov/leaflet-plugins), allows for 9 | displaying and analyzing GPX tracks and their waypoints so they can be 10 | displayed on a Leaflet map as a new layer. 11 | 12 | As it parses the GPX data, `leaflet-gpx` records information about the 13 | GPX track, including total time, moving time, total distance, elevation 14 | stats and heart-rate, and makes it accessible through an exhaustive set 15 | of accessor methods. 16 | 17 | GPX parsing will automatically handle pauses in the track with a default 18 | tolerance interval of 15 seconds between points. You can configure this 19 | interval by setting `max_point_interval`, in milliseconds, in the options 20 | passed to the `GPX` constructor. 21 | 22 | I've put together a complete example as a 23 | [demo](http://mpetazzoni.github.io/leaflet-gpx/). 24 | 25 | ## License 26 | 27 | `leaflet-gpx` is under the *BSD 2-clause license*. Please refer to the 28 | attached LICENSE file and/or to the copyright header in gpx.js for more 29 | information. 30 | 31 | ## Usage 32 | 33 | Usage is very simple: 34 | 35 | * Include the Leaflet stylesheet and script, and the leaflet-gpx script, 36 | in your HTML page; 37 | * Create your Leaflet map, with your choice of base layer(s); 38 | * Create the `L.GPX` layer to display your GPX track. 39 | 40 | ```html 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 |
52 | 53 | 70 | 71 | 72 | ``` 73 | 74 | ### Importing from a non-module context 75 | 76 | ```javascript 77 | const map = L.map('map'); 78 | L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 79 | attribution: 'Map data © OpenStreetMap' 80 | }).addTo(map); 81 | 82 | await import('gpx.js').then((module) => { 83 | new L.GPX('https://...').on('loaded', (e) => { 84 | map.fitBounds(e.target.getBounds()); 85 | }).addTo(map); 86 | }); 87 | ``` 88 | 89 | ## Functions 90 | 91 | If you want to display additional information about the GPX track, you can do 92 | so in the 'loaded' event handler, calling one of the following methods on the 93 | `GPX` object `e.target`: 94 | 95 | * `get_name()`: returns the name of the GPX track 96 | * `get_distance()`: returns the total track distance, in meters 97 | * `get_start_time()`: returns a Javascript `Date` object representing the 98 | starting time 99 | * `get_end_time()`: returns a Javascript `Date` object representing when the 100 | last point was recorded 101 | * `get_moving_time()`: returns the moving time, in milliseconds 102 | * `get_total_time()`: returns the total track time, in milliseconds 103 | * `get_moving_pace()`: returns the average moving pace in milliseconds per km 104 | * `get_moving_speed()`: returns the average moving speed in km per hour 105 | * `get_total_speed()`: returns the average total speed in km per hour 106 | * `get_elevation_min()`: returns the lowest elevation, in meters 107 | * `get_elevation_max()`: returns the highest elevation, in meters 108 | * `get_elevation_gain()`: returns the cumulative elevation gain, in meters 109 | * `get_elevation_loss()`: returns the cumulative elevation loss, in meters 110 | * `get_speed_max()`: returns the maximum speed in km per hour 111 | * `get_average_hr()`: returns the average heart rate (if available) 112 | * `get_average_cadence()`: returns the average cadence (if available) 113 | * `get_average_temp()`: returns the average of the temperature (if available) 114 | 115 | If you're not a fan of the metric system, you also have the following methods 116 | at your disposal: 117 | 118 | * `get_distance_imp()`: returns the total track distance in miles 119 | * `get_moving_pace_imp()`: returns the average moving pace in milliseconds per 120 | mile 121 | * `get_moving_speed_imp()`: returns the average moving speed in miles per hour 122 | * `get_total_speed_imp()`: returns the average total speed in miles per hour 123 | * `get_elevation_min_imp()`: returns the lowest elevation, in feet 124 | * `get_elevation_max_imp()`: returns the highest elevation, in feet 125 | * `get_elevation_gain_imp()`: returns the cumulative elevation gain, in feet 126 | * `get_elevation_loss_imp()`: returns the cumulative elevation loss, in feet 127 | * `get_speed_max_imp()`: returns the maximum speed in miles per hour 128 | 129 | The reason why these methods return milliseconds is that you have at your 130 | disposal nice helper methods to format a duration in milliseconds into a cool 131 | string: 132 | 133 | * `get_duration_string(duration, hidems)` format to a string like `3:07'48"` 134 | or `59'32.431`, where `duration` is in 135 | milliseconds and `hidems` is an optional boolean you can use to request never 136 | to display millisecond precision. 137 | * `get_duration_string_iso(duration, hidems)` formats to an ISO like 138 | representation like `3:07:48` or `59:32.431`, where `duration` is in 139 | milliseconds and `hidems` is an optional boolean you can use to request never 140 | to display millisecond precision. 141 | 142 | You can also get full elevation, heartrate, cadence and temperature data with: 143 | 144 | * `get_elevation_data()` and `get_elevation_data_imp()` 145 | * `get_speed_data()` and `get_speed_data_imp()` 146 | * `get_heartrate_data()` and `get_heartrate_data_imp()` 147 | * `get_cadence_data()` and `get_cadence_data_imp()` 148 | * `get_temp_data()` and `get_temp_data_imp()` 149 | 150 | These methods all return an array of points `[distance, value, tooltip]` where 151 | the distance is either in kilometers or in miles and the elevation in meters or 152 | feet, depending on whether you use the `_imp` variant or not. Heart rate, 153 | obviously, doesn't change. 154 | 155 | ## Reloading 156 | 157 | You can make `leaflet-gpx` reload the source GPX file by calling the 158 | `reload()` method. For example, to trigger a reload every 5 seconds, you 159 | can do: 160 | 161 | ```javascript 162 | var gpx = new L.GPX(url); 163 | setInterval(function() { 164 | gpx.reload(); 165 | }, 5000); 166 | ``` 167 | 168 | ## About marker icons 169 | 170 | ### Configuring markers 171 | 172 | By default, `leaflet-gpx` uses Leaflet's default icon image for all 173 | markers. You can override this behavior by providing a Leaflet `Icon` 174 | object, or the path or URL to an image to use as the marker, for any of 175 | the markers supported by this plugin as part of the `markers` parameter: 176 | 177 | ```javascript 178 | new L.GPX(url, { 179 | async: true, 180 | markers: { 181 | startIcon: ..., 182 | endIcon: ... 183 | wptIcons: { ... }, 184 | wptTypeIcons: { ... }, 185 | pointMatchers: [ ... ], 186 | } 187 | }).on('loaded', function(e) { 188 | map.fitBounds(e.target.getBounds()); 189 | }).addTo(map); 190 | ``` 191 | 192 | * `startIcon` is used for the marker at the beginning of the GPX track; 193 | * `endIcon` is used for the marker at the end of the GPX track; 194 | * `wptIcons` and `wptTypeIcons` are mappings of waypoint symbols and 195 | types to the icon you want to use for each; 196 | * `pointMatchers` is an array of custom point matchers and their 197 | respective icon (see below); 198 | 199 | You can also override any of those to `null` to disable the 200 | corresponding marker altogether. 201 | 202 | Here is how you would override the URL of the provided icons for start 203 | and end markers, but none of the other types of markers: 204 | 205 | ```javascript 206 | new L.GPX(url, { 207 | async: true, 208 | markers: { 209 | startIcon: 'images/pin-icon-start.png', 210 | endIcon: 'images/pin-icon-end.png', 211 | } 212 | }).on('loaded', function(e) { 213 | map.fitBounds(e.target.getBounds()); 214 | }).addTo(map); 215 | ``` 216 | 217 | It's usually preferrable and more flexible to provide a Leaflet `Icon` 218 | instance directly, for example from 219 | [leaflet-awesome-markers](https://github.com/lennardv2/Leaflet.awesome-markers). See 220 | for more information. 221 | 222 | ```javascript 223 | new L.GPX(url, { 224 | async: true, 225 | markers: { 226 | wptIcons: { 227 | 'Coffee shop': new L.AwesomeMarkers.icon({ 228 | icon: 'coffee', 229 | prefix: 'fa', 230 | markerColor: 'blue', 231 | iconColor: 'white' 232 | }) 233 | } 234 | } 235 | }).on('loaded', function (e) { 236 | map.fitBounds(e.target.getBounds()); 237 | }).addTo(map); 238 | ``` 239 | 240 | ### Marker options 241 | 242 | You can fine tune marker options using any of the parameters expected by 243 | [Leaflet's base L.Icon class](https://leafletjs.com/reference.html#icon) 244 | using the `marker_options` parameters: 245 | 246 | ```javascript 247 | new L.GPX(url, { 248 | async: true, 249 | marker_options: { 250 | iconSize: [38, 95], 251 | iconAnchor: [22, 94], 252 | } 253 | }).on('loaded', function(e) { 254 | map.fitBounds(e.target.getBounds()); 255 | }).addTo(map); 256 | ``` 257 | 258 | ### Sensible defaults 259 | 260 | Note that you do not need to override all the marker definitions, or 261 | marker options, when providing the `markers` and `marker_options` 262 | parameters to the GPX constructor as this plugin will use sensible 263 | defaults for all of those settings. 264 | 265 | ## About waypoints 266 | 267 | By default, this plugin will parse all Waypoints from a GPX file. This 268 | can be controlled via the value `waypoint` in `gpx_options`, e.g. 269 | `parseElements: ['track', 'route', 'waypoint']`. 270 | 271 | The icon used in the marker representing each track waypoint is 272 | determined based on the waypoint's properties, in this order: 273 | 274 | * If the waypoint has a `sym` attribute, the `markers.wptIcons[sym]` 275 | icon is used; 276 | * If the waypoint has a `type` attribute, the `markers.wptTypeIcons[type]` 277 | icon is used; 278 | * Point matchers are evaluated in order, if one matches the waypoint's 279 | `name` attribute, its icon is used (see _Named markers_ below); 280 | * If none of the above rules match, the default `''` (empty string) icon 281 | entry in `wptIcons` is used. 282 | 283 | ```javascript 284 | new L.GPX(url, { 285 | async: true, 286 | markers: { 287 | wptIcons: { 288 | '': new L.Icon.Default, 289 | 'Geocache Found': 'img/gpx/geocache.png', 290 | 'Park': 'img/gpx/tree.png' 291 | }, 292 | } 293 | }).on('loaded', function (e) { 294 | map.fitBounds(e.target.getBounds()); 295 | }).addTo(map); 296 | ``` 297 | 298 | ## Named points 299 | 300 | GPX points can be named, for example to denote certain POIs (points of 301 | interest). You can setup rules to match point names to create labeled 302 | markers for those points by providing a `pointMatchers` array in the 303 | `markers` constructor parameter. 304 | 305 | Each element in this array must define a `regex` to match the point's 306 | name and an `icon` definition (a `L.Icon` or subclass object, or the URL 307 | to an icon image). 308 | 309 | Each named point in the GPX track is evaluated against those rules and 310 | a marker is created with the point's name as label from the first 311 | matching rule. This also applies to named waypoints, but keep in mind 312 | that waypoint icons rules take precedence over point matchers. 313 | 314 | ```javascript 315 | new L.GPX(url, { 316 | async: true, 317 | markers: { 318 | pointMatchers: [ 319 | { 320 | regex: /Coffee/, 321 | icon: new L.AwesomeMarkers.icon({ 322 | icon: 'coffee', 323 | markerColor: 'blue', 324 | iconColor: 'white' 325 | }), 326 | }, 327 | { 328 | regex: /Home/, 329 | icon: new L.AwesomeMarkers.icon({ 330 | icon: 'home', 331 | markerColor: 'green', 332 | iconColor: 'white' 333 | }), 334 | } 335 | ] 336 | } 337 | }).on('loaded', function(e) { 338 | map.fitToBounds(e.target.getBounds()); 339 | }).addTo(map); 340 | ``` 341 | 342 | ## Events 343 | 344 | Events are fired on the `L.GPX` object as the GPX data is being parsed 345 | and the map layers generated. You can listen for those events by 346 | attaching the corresponding event listener on the `L.GPX` object: 347 | 348 | ```javascript 349 | new L.GPX(url, async: true, { 350 | // options 351 | }).on('addpoint', function(e) { 352 | console.log('Added ' + e.point_type + ' point: ' + e.point); 353 | }).on('loaded', function(e) { 354 | var gpx = e.target; 355 | map.fitToBounds(gpx.getBounds()); 356 | }).on('error', function(e) { 357 | console.log('Error loading file: ' + e.err); 358 | }).addTo(map); 359 | ``` 360 | 361 | Note that for your event listeners to be correctly triggered, you need 362 | to pass `async: true` to the `L.GPX` constructor; otherwise the parsing 363 | of the GPX happens synchronously in the constructor before you your 364 | event listeners get registered! 365 | 366 | `addpoint` events are fired for every marker added to the map, in 367 | particular for the start and end points, all the waypoints, and all the 368 | named points that matched `pointMatchers` rules. Each `addpoint` event 369 | contains the following properties: 370 | 371 | - `point`: the marker object itself, from which you can get or modify 372 | the latitude and longitude of the point and any other attribute of the 373 | marker. 374 | - `point_type`: one of `start`, `end`, `waypoint` or `label`, allowing 375 | you to identify what type of point the marker is for. 376 | - `element`: the track point element the marker was created for. 377 | 378 | One use case for those events is for example to attach additional 379 | content or behavior to the markers that were generated (popups, etc). 380 | 381 | `error` events are fired when no layers of the type(s) specified in 382 | `options.gpx_options.parseElements` can be parsed out of the given 383 | file. For instance, `error` would be fired if a file with no waypoints 384 | was attempted to be loaded with `parseElements` set to `['waypoint']`. 385 | Each `error` event contains the following property: 386 | 387 | - `err`: a message with details about the error that occurred. 388 | 389 | ## Line styling 390 | 391 | `leaflet-gpx` understands the [GPX 392 | Style](http://www.topografix.com/GPX/gpx_style/0/2) extension, and will 393 | extract styling information defined on routes and track segments to use 394 | for drawing the corresponding polyline. 395 | 396 | ```xml 397 | 398 | 399 | 400 | FF0000 401 | 0.5 402 | 1 403 | square 404 | square 405 | 0,10 406 | 3 407 | 408 | 409 | 410 | 411 | ``` 412 | 413 | You can override the style of the lines by passing a `polyline_options` 414 | array into the `options` argument of the `L.GPX` constructor, each 415 | element of the array defines the style for the corresponding route 416 | and/or track in the file (in the same order). 417 | 418 | ```javascript 419 | new L.GPX(url, { 420 | polyline_options: [{ 421 | color: 'green', 422 | opacity: 0.75, 423 | weight: 3, 424 | lineCap: 'round' 425 | },{ 426 | color: 'blue', 427 | opacity: 0.75, 428 | weight: 1 429 | }] 430 | }).on('loaded', function(e) { 431 | var gpx = e.target; 432 | map.fitToBounds(gpx.getBounds()); 433 | }).addTo(map); 434 | ``` 435 | 436 | If you have many routes or tracks in your GPX file and you want them to 437 | share the same styling, you can pass `polyline_options` as a single 438 | object rather than an array (this is also how `leaflet-gpx` worked 439 | before the introduction of the array): 440 | 441 | ```javascript 442 | new L.GPX(url, { 443 | polyline_options: { 444 | color: 'green', 445 | opacity: 0.75, 446 | weight: 3, 447 | lineCap: 'round' 448 | } 449 | }).on('loaded', function(e) { 450 | var gpx = e.target; 451 | map.fitToBounds(gpx.getBounds()); 452 | }).addTo(map); 453 | ``` 454 | 455 | For more information on the available polyline styling options, refer to 456 | the [Leaflet documentation on 457 | Polyline](https://leafletjs.com/reference.html#polyline). By 458 | default, if no styling is available, the line will be drawn in _blue_. 459 | 460 | ## GPX parsing options 461 | 462 | ### Selecting which elements define the track 463 | 464 | Some GPX tracks contain the actual route/track twice, both the `` 465 | and `` elements are used. You can tell `leaflet-gpx` which tag to 466 | use or to use both (which is the default setting for backwards 467 | compatibility). The `parseElements` field of `gpx_options` controls this 468 | behavior. It should be an array that contains `'route'` and/or `'track'` 469 | and/or `'waypoint'`. 470 | 471 | ### Multiple track segments within each track 472 | 473 | GPX file may contain multiple tracks represented by `` elements, 474 | each track possibly composed of multiple segments with `` 475 | elements. Although this plugin will always represent each GPX route and 476 | each GPX track as distinct entities with their own start and end 477 | markers, track segments will by default be joined into a single line. 478 | 479 | You can disable this behavior by setting the `joinTrackSegments` flag to 480 | `false` in the `gpx_options`: 481 | 482 | ```javascript 483 | new L.GPX(url, { 484 | gpx_options: { 485 | joinTrackSegments: false 486 | } 487 | }).on('loaded', function(e) { 488 | map.fitBounds(e.target.getBounds()); 489 | }).addTo(map); 490 | ``` 491 | 492 | ## Caveats 493 | 494 | * Distance calculation is relatively accurate, but elevation change 495 | calculation is not topographically adjusted, so the total elevation 496 | gain/loss/change might appear inaccurate in some situations. 497 | * Currently doesn't seem to work in IE8/9. See #9 and #11 for 498 | discussion. 499 | -------------------------------------------------------------------------------- /gpx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2011-2012 Pavel Shramov 3 | * Copyright (C) 2013-2017 Maxime Petazzoni 4 | * All Rights Reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * - Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * - Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | * POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | /* 30 | * Thanks to Pavel Shramov who provided the initial implementation and Leaflet 31 | * integration. Original code was at https://github.com/shramov/leaflet-plugins. 32 | * 33 | * It was then cleaned-up and modified to record and make available more 34 | * information about the GPX track while it is being parsed so that the result 35 | * can be used to display additional information about the track that is 36 | * rendered on the Leaflet map. 37 | */ 38 | 39 | 'use strict'; 40 | 41 | var _MAX_POINT_INTERVAL_MS = 15000; 42 | var _SECOND_IN_MILLIS = 1000; 43 | var _MINUTE_IN_MILLIS = 60 * _SECOND_IN_MILLIS; 44 | var _HOUR_IN_MILLIS = 60 * _MINUTE_IN_MILLIS; 45 | var _DAY_IN_MILLIS = 24 * _HOUR_IN_MILLIS; 46 | 47 | var _GPX_STYLE_NS = 'http://www.topografix.com/GPX/gpx_style/0/2'; 48 | var _DEFAULT_ICON = new L.Icon.Default; 49 | 50 | var _DEFAULT_MARKERS = { 51 | startIcon: _DEFAULT_ICON, 52 | endIcon: _DEFAULT_ICON, 53 | 54 | // Based on 'sym' waypoint key 55 | wptIcons: { 56 | '': _DEFAULT_ICON, 57 | }, 58 | 59 | // Based on 'type' waypoint key 60 | wptTypeIcons: { 61 | '': _DEFAULT_ICON 62 | }, 63 | 64 | // Based on a regex over the waypoint's name 65 | pointMatchers: [], 66 | }; 67 | 68 | var _DEFAULT_MARKER_OPTS = { 69 | iconSize: [33, 45], 70 | iconAnchor: [16, 45], 71 | clickable: false 72 | }; 73 | 74 | var _DEFAULT_POLYLINE_OPTS = { 75 | color: 'blue' 76 | }; 77 | 78 | var _DEFAULT_GPX_OPTS = { 79 | parseElements: ['track', 'route', 'waypoint'], 80 | joinTrackSegments: true 81 | }; 82 | 83 | L.GPX = L.FeatureGroup.extend({ 84 | initialize: function(gpx, options) { 85 | options.max_point_interval = options.max_point_interval || _MAX_POINT_INTERVAL_MS; 86 | options.markers = this._merge_objs( 87 | _DEFAULT_MARKERS, 88 | options.markers || {}); 89 | options.marker_options = this._merge_objs( 90 | _DEFAULT_MARKER_OPTS, 91 | options.marker_options || {}); 92 | options.polyline_options = options.polyline_options || []; 93 | options.gpx_options = this._merge_objs( 94 | _DEFAULT_GPX_OPTS, 95 | options.gpx_options || {}); 96 | 97 | L.Util.setOptions(this, options); 98 | 99 | // Base icon class for track pins. 100 | L.GPXTrackIcon = L.Icon.extend({ options: options.marker_options }); 101 | 102 | this._gpx = gpx; 103 | this._layers = {}; 104 | this._prepare_markers(options.markers); 105 | this._init_info(); 106 | 107 | if (gpx) { 108 | this._parse(gpx, options, this.options.async); 109 | } 110 | }, 111 | 112 | get_duration_string: function(duration, hidems) { 113 | var s = ''; 114 | 115 | if (duration >= _DAY_IN_MILLIS) { 116 | s += Math.floor(duration / _DAY_IN_MILLIS) + 'd '; 117 | duration = duration % _DAY_IN_MILLIS; 118 | } 119 | 120 | if (duration >= _HOUR_IN_MILLIS) { 121 | s += Math.floor(duration / _HOUR_IN_MILLIS) + ':'; 122 | duration = duration % _HOUR_IN_MILLIS; 123 | } 124 | 125 | var mins = Math.floor(duration / _MINUTE_IN_MILLIS); 126 | duration = duration % _MINUTE_IN_MILLIS; 127 | if (mins < 10) s += '0'; 128 | s += mins + '\''; 129 | 130 | var secs = Math.floor(duration / _SECOND_IN_MILLIS); 131 | duration = duration % _SECOND_IN_MILLIS; 132 | if (secs < 10) s += '0'; 133 | s += secs; 134 | 135 | if (!hidems && duration > 0) s += '.' + Math.round(Math.floor(duration)*1000)/1000; 136 | else s += '"'; 137 | 138 | return s; 139 | }, 140 | 141 | get_duration_string_iso: function(duration, hidems) { 142 | var s = this.get_duration_string(duration, hidems); 143 | return s.replace("'",':').replace('"',''); 144 | }, 145 | _toFixed_helper: function(v, unit = 0) { 146 | if (typeof(v) == 'number'){ 147 | return v.toFixed(unit); 148 | } 149 | return "?"; 150 | }, 151 | // Public methods 152 | to_miles: function(v) { return v / 1.60934; }, 153 | to_ft: function(v) { return v * 3.28084; }, 154 | m_to_km: function(v) { return v / 1000; }, 155 | m_to_mi: function(v) { return v / 1609.34; }, 156 | ms_to_kmh: function(v) { return v * 3.6; }, 157 | ms_to_mih: function(v) { return v / 1609.34 * 3600; }, 158 | 159 | get_name: function() { return this._info.name; }, 160 | get_desc: function() { return this._info.desc; }, 161 | get_author: function() { return this._info.author; }, 162 | get_copyright: function() { return this._info.copyright; }, 163 | get_distance: function() { return this._info.length; }, 164 | get_distance_imp: function() { return this.to_miles(this.m_to_km(this.get_distance())); }, 165 | 166 | get_start_time: function() { return this._info.duration.start; }, 167 | get_end_time: function() { return this._info.duration.end; }, 168 | get_moving_time: function() { return this._info.duration.moving; }, 169 | get_total_time: function() { return this._info.duration.total; }, 170 | 171 | get_moving_pace: function() { return this.get_moving_time() / this.m_to_km(this.get_distance()); }, 172 | get_moving_pace_imp: function() { return this.get_moving_time() / this.get_distance_imp(); }, 173 | 174 | get_moving_speed: function() { return this.m_to_km(this.get_distance()) / (this.get_moving_time() / (3600 * 1000)) ; }, 175 | get_moving_speed_imp:function() { return this.to_miles(this.m_to_km(this.get_distance())) / (this.get_moving_time() / (3600 * 1000)) ; }, 176 | 177 | get_total_speed: function() { return this.m_to_km(this.get_distance()) / (this.get_total_time() / (3600 * 1000)); }, 178 | get_total_speed_imp: function() { return this.to_miles(this.m_to_km(this.get_distance())) / (this.get_total_time() / (3600 * 1000)); }, 179 | 180 | get_elevation_gain: function() { return this._info.elevation.gain; }, 181 | get_elevation_loss: function() { return this._info.elevation.loss; }, 182 | get_elevation_gain_imp: function() { return this.to_ft(this.get_elevation_gain()); }, 183 | get_elevation_loss_imp: function() { return this.to_ft(this.get_elevation_loss()); }, 184 | get_elevation_data: function() { 185 | var _this = this; 186 | return this._info.elevation._points.map( 187 | function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, 188 | function(a, b) { return _this._toFixed_helper(a, 2) + ' km, ' + _this._toFixed_helper(b, 0) + ' m'; }); 189 | }); 190 | }, 191 | get_elevation_data_imp: function() { 192 | var _this = this; 193 | return this._info.elevation._points.map( 194 | function(p) { return _this._prepare_data_point(p, _this.m_to_mi, _this.to_ft, 195 | function(a, b) { return _this._toFixed_helper(a, 2) + ' mi, ' + _this._toFixed_helper(b, 0) + ' ft'; }); 196 | }); 197 | }, 198 | get_elevation_max: function() { return this._info.elevation.max; }, 199 | get_elevation_min: function() { return this._info.elevation.min; }, 200 | get_elevation_max_imp: function() { return this.to_ft(this.get_elevation_max()); }, 201 | get_elevation_min_imp: function() { return this.to_ft(this.get_elevation_min()); }, 202 | 203 | get_speed_data: function() { 204 | var _this = this; 205 | return this._info.speed._points.map( 206 | function(p) { return _this._prepare_data_point(p, _this.m_to_km, _this.ms_to_kmh, 207 | function(a, b) { return _this._toFixed_helper(a, 2) + ' km, ' + _this._toFixed_helper(b, 2) + ' km/h'; }); 208 | }); 209 | }, 210 | get_speed_data_imp: function() { 211 | var _this = this; 212 | return this._info.speed._points.map( 213 | function(p) { return _this._prepare_data_point(p, _this.m_to_mi, _this.ms_to_mih, 214 | function(a, b) { return _this._toFixed_helper(a, 2) + ' mi, ' + _this._toFixed_helper(b, 2) + ' mi/h'; }); 215 | }); 216 | }, 217 | get_speed_max: function() { return this.m_to_km(this._info.speed.max) * 3600; }, 218 | get_speed_max_imp: function() { return this.to_miles(this.get_speed_max()); }, 219 | 220 | get_average_hr: function() { return this._info.hr.avg; }, 221 | get_average_temp: function() { return this._info.atemp.avg; }, 222 | get_average_cadence: function() { return this._info.cad.avg; }, 223 | get_heartrate_data: function() { 224 | var _this = this; 225 | return this._info.hr._points.map( 226 | function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, 227 | function(a, b) { return _this._toFixed_helper(a, 2) + ' km, ' + _this._toFixed_helper(b, 0) + ' bpm'; }); 228 | }); 229 | }, 230 | get_heartrate_data_imp: function() { 231 | var _this = this; 232 | return this._info.hr._points.map( 233 | function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, 234 | function(a, b) { return _this._toFixed_helper(a, 2) + ' mi, ' + _this._toFixed_helper(b, 0) + ' bpm'; }); 235 | }); 236 | }, 237 | get_cadence_data: function() { 238 | var _this = this; 239 | return this._info.cad._points.map( 240 | function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, 241 | function(a, b) { return _this._toFixed_helper(a, 2) + ' km, ' + _this._toFixed_helper(b, 0) + ' rpm'; }); 242 | }); 243 | }, 244 | get_temp_data: function() { 245 | var _this = this; 246 | return this._info.atemp._points.map( 247 | function(p) { return _this._prepare_data_point(p, _this.m_to_km, null, 248 | function(a, b) { return _this._toFixed_helper(a, 2) + ' km, ' + _this._toFixed_helper(b, 0) + ' degrees'; }); 249 | }); 250 | }, 251 | get_cadence_data_imp: function() { 252 | var _this = this; 253 | return this._info.cad._points.map( 254 | function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, 255 | function(a, b) { return _this._toFixed_helper(a, 2) + ' mi, ' + _this._toFixed_helper(b, 0) + ' rpm'; }); 256 | }); 257 | }, 258 | get_temp_data_imp: function() { 259 | var _this = this; 260 | return this._info.atemp._points.map( 261 | function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null, 262 | function(a, b) { return _this._toFixed_helper(a, 2) + ' mi, ' + _this._toFixed_helper(b, 0) + ' degrees'; }); 263 | }); 264 | }, 265 | 266 | reload: function() { 267 | this._init_info(); 268 | this.clearLayers(); 269 | this._parse(this._gpx, this.options, this.options.async); 270 | }, 271 | 272 | // Private methods 273 | _merge_objs: function(a, b) { 274 | var _ = {}; 275 | for (var attr in a) { _[attr] = a[attr]; } 276 | for (var attr in b) { _[attr] = b[attr]; } 277 | return _; 278 | }, 279 | 280 | _prepare_data_point: function(p, trans1, trans2, trans_tooltip) { 281 | var r = [trans1 && trans1(p[0]) || p[0], trans2 && trans2(p[1]) || p[1]]; 282 | r.push(trans_tooltip && trans_tooltip(r[0], r[1]) || (r[0] + ': ' + r[1])); 283 | return r; 284 | }, 285 | 286 | _prepare_markers: function(markers) { 287 | function iconize(url) { 288 | return new L.GPXTrackIcon({iconUrl: url}); 289 | } 290 | 291 | Object.entries(markers).forEach(([key, value]) => { 292 | if (key === 'wptIcons' || key === 'wptTypeIcons') { 293 | markers[key] = this._prepare_markers(value); 294 | } else if (key === 'pointMatchers') { 295 | markers[key] = value.map(e => { 296 | if (typeof(e.icon) === 'string') { 297 | e.icon = iconize(e.icon); 298 | } 299 | return e; 300 | }); 301 | } else if (typeof(value) === 'string') { 302 | markers[key] = iconize(value); 303 | } else if (typeof(value) === 'object' && value !== null) { 304 | markers[key] = value; 305 | } 306 | }); 307 | 308 | return markers; 309 | }, 310 | 311 | _init_info: function() { 312 | this._info = { 313 | name: null, 314 | length: 0.0, 315 | elevation: {gain: 0.0, loss: 0.0, max: 0.0, min: Infinity, _points: []}, 316 | speed : {max: 0.0, _points: []}, 317 | hr: {avg: 0, _total: 0, _points: []}, 318 | duration: {start: null, end: null, moving: 0, total: 0}, 319 | atemp: {avg: 0, _total: 0, _points: []}, 320 | cad: {avg: 0, _total: 0, _points: []} 321 | }; 322 | }, 323 | 324 | _load_xml: function(url, cb, options, async) { 325 | if (async == undefined) async = this.options.async; 326 | if (options == undefined) options = this.options; 327 | 328 | var _this = this; 329 | var req = new window.XMLHttpRequest(); 330 | req.open('GET', url, async); 331 | try { 332 | req.overrideMimeType('text/xml'); // unsupported by IE 333 | } catch(e) {} 334 | req.onloadend = function() { 335 | if (req.status == 200) { 336 | cb(req.responseXML, options); 337 | } else { 338 | _this.fire('error', { err: 'Error fetching resource: ' + url }); 339 | } 340 | }; 341 | req.send(null); 342 | }, 343 | 344 | _parse: function(input, options, async) { 345 | var _this = this; 346 | var cb = function(gpx, options) { 347 | var layers = _this._parse_gpx_data(gpx, options); 348 | if (!layers) { 349 | _this.fire('error', { err: 'No parseable layers of type(s) ' + JSON.stringify(options.gpx_options.parseElements) }); 350 | return; 351 | } 352 | _this.addLayer(layers); 353 | _this.fire('loaded', { layers: layers, element: gpx }); 354 | } 355 | if (input.substr(0,1)==='<') { // direct XML has to start with a < 356 | var parser = new DOMParser(); 357 | if (async) { 358 | setTimeout(function() { 359 | cb(parser.parseFromString(input, "text/xml"), options); 360 | }); 361 | } else { 362 | cb(parser.parseFromString(input, "text/xml"), options); 363 | } 364 | } else { 365 | this._load_xml(input, cb, options, async); 366 | } 367 | }, 368 | 369 | _parse_gpx_data: function(xml, options) { 370 | var i, t, l, el, layers = []; 371 | 372 | var name = xml.getElementsByTagName('name'); 373 | if (name.length > 0) { 374 | this._info.name = name[0].textContent; 375 | } 376 | var desc = xml.getElementsByTagName('desc'); 377 | if (desc.length > 0) { 378 | this._info.desc = desc[0].textContent; 379 | } 380 | var author = xml.getElementsByTagName('author'); 381 | if (author.length > 0) { 382 | this._info.author = author[0].textContent; 383 | } 384 | var copyright = xml.getElementsByTagName('copyright'); 385 | if (copyright.length > 0) { 386 | this._info.copyright = copyright[0].textContent; 387 | } 388 | 389 | var parseElements = options.gpx_options.parseElements; 390 | if (parseElements.indexOf('route') > -1) { 391 | // routes are tags inside sections 392 | var routes = xml.getElementsByTagName('rte'); 393 | for (i = 0; i < routes.length; i++) { 394 | var route = routes[i]; 395 | var base_style = this._extract_styling(route); 396 | var polyline_options = this._get_polyline_options(options.polyline_options, i); 397 | layers = layers.concat(this._parse_segment(routes[i], options, base_style, polyline_options, 'rtept')); 398 | } 399 | } 400 | 401 | if (parseElements.indexOf('track') > -1) { 402 | // tracks are tags in one or more sections in each 403 | var tracks = xml.getElementsByTagName('trk'); 404 | for (i = 0; i < tracks.length; i++) { 405 | var track = tracks[i]; 406 | var base_style = this._extract_styling(track); 407 | var polyline_options = this._get_polyline_options(options.polyline_options, i); 408 | 409 | if (options.gpx_options.joinTrackSegments) { 410 | layers = layers.concat(this._parse_segment(track, options, base_style, polyline_options, 'trkpt')); 411 | } else { 412 | var segments = track.getElementsByTagName('trkseg'); 413 | for (j = 0; j < segments.length; j++) { 414 | layers = layers.concat(this._parse_segment(segments[j], options, base_style, polyline_options, 'trkpt')); 415 | } 416 | } 417 | } 418 | } 419 | 420 | this._info.hr.avg = Math.round(this._info.hr._total / this._info.hr._points.length); 421 | this._info.cad.avg = Math.round(this._info.cad._total / this._info.cad._points.length); 422 | this._info.atemp.avg = Math.round(this._info.atemp._total / this._info.atemp._points.length); 423 | 424 | // parse waypoints and add markers for each of them 425 | if (parseElements.indexOf('waypoint') > -1) { 426 | el = xml.getElementsByTagName('wpt'); 427 | for (i = 0; i < el.length; i++) { 428 | var ll = new L.LatLng( 429 | el[i].getAttribute('lat'), 430 | el[i].getAttribute('lon')); 431 | 432 | var nameEl = el[i].getElementsByTagName('name'); 433 | var name = nameEl.length > 0 ? nameEl[0].textContent : ''; 434 | 435 | var descEl = el[i].getElementsByTagName('desc'); 436 | var desc = descEl.length > 0 ? descEl[0].textContent : ''; 437 | 438 | var symEl = el[i].getElementsByTagName('sym'); 439 | var symKey = symEl.length > 0 ? symEl[0].textContent : null; 440 | 441 | var typeEl = el[i].getElementsByTagName('type'); 442 | var typeKey = typeEl.length > 0 ? typeEl[0].textContent : null; 443 | 444 | /* 445 | * Add waypoint marker based on the waypoint symbol key. 446 | * 447 | * First look for a configured icon for that symKey. If not found, look 448 | * for a configured icon URL for that symKey and build an icon from it. 449 | * If none of those match, look through the point matchers for a match 450 | * on the waypoint's name. 451 | * 452 | * Otherwise, fall back to the default icon if one was configured, or 453 | * finally to the default icon URL, if one was configured. 454 | */ 455 | var wptIcons = options.markers.wptIcons; 456 | var wptTypeIcons = options.markers.wptTypeIcons; 457 | var ptMatchers = options.markers.pointMatchers || []; 458 | var symIcon; 459 | if (wptIcons && symKey && wptIcons[symKey]) { 460 | symIcon = wptIcons[symKey]; 461 | } else if (wptTypeIcons && typeKey && wptTypeIcons[typeKey]) { 462 | symIcon = wptTypeIcons[typeKey]; 463 | } else if (ptMatchers.length > 0) { 464 | for (var j = 0; j < ptMatchers.length; j++) { 465 | if (ptMatchers[j].regex.test(name)) { 466 | symIcon = ptMatchers[j].icon; 467 | break; 468 | } 469 | } 470 | } else if (wptIcons && wptIcons['']) { 471 | symIcon = wptIcons['']; 472 | } 473 | 474 | if (!symIcon) { 475 | console.log( 476 | 'No waypoint icon could be matched for symKey=%s,typeKey=%s,name=%s on waypoint %o', 477 | symKey, typeKey, name, el[i]); 478 | continue; 479 | } 480 | 481 | var marker = new L.Marker(ll, { 482 | clickable: options.marker_options.clickable, 483 | title: name, 484 | icon: symIcon, 485 | type: 'waypoint' 486 | }); 487 | marker.bindPopup("" + name + "" + (desc.length > 0 ? '
' + desc : '')).openPopup(); 488 | this.fire('addpoint', { point: marker, point_type: 'waypoint', element: el[i] }); 489 | layers.push(marker); 490 | } 491 | } 492 | 493 | if (layers.length > 1) { 494 | return new L.FeatureGroup(layers); 495 | } else if (layers.length == 1) { 496 | return layers[0]; 497 | } 498 | }, 499 | 500 | _parse_segment: function(line, options, base_style, polyline_options, tag) { 501 | var el = line.getElementsByTagName(tag); 502 | if (!el.length) return []; 503 | 504 | var coords = []; 505 | var markers = []; 506 | var layers = []; 507 | var last = null; 508 | 509 | for (var i = 0; i < el.length; i++) { 510 | var _, ll = new L.LatLng( 511 | el[i].getAttribute('lat'), 512 | el[i].getAttribute('lon')); 513 | ll.meta = { time: null, ele: null, hr: null, cad: null, atemp: null, speed: null }; 514 | 515 | _ = el[i].getElementsByTagName('time'); 516 | if (_.length > 0) { 517 | ll.meta.time = new Date(Date.parse(_[0].textContent)); 518 | } else { 519 | ll.meta.time = new Date('1970-01-01T00:00:00'); 520 | } 521 | var time_diff = last != null ? Math.abs(ll.meta.time - last.meta.time) : 0; 522 | 523 | _ = el[i].getElementsByTagName('ele'); 524 | if (_.length > 0) { 525 | ll.meta.ele = parseFloat(_[0].textContent); 526 | } else { 527 | // If the point doesn't have an tag, assume it has the same 528 | // elevation as the point before it (if it had one). 529 | ll.meta.ele = last != null ? last.meta.ele : null; 530 | } 531 | var ele_diff = last != null ? ll.meta.ele - last.meta.ele : 0; 532 | var dist_3d = last != null ? this._dist3d(last, ll) : 0; 533 | 534 | _ = el[i].getElementsByTagName('speed'); 535 | if (_.length > 0) { 536 | ll.meta.speed = parseFloat(_[0].textContent); 537 | } else { 538 | // speed in meter per second 539 | ll.meta.speed = time_diff > 0 ? 1000.0 * dist_3d / time_diff : 0; 540 | } 541 | 542 | _ = el[i].getElementsByTagName('name'); 543 | if (_.length > 0) { 544 | var name = _[0].textContent; 545 | var ptMatchers = options.markers.pointMatchers || []; 546 | 547 | for (var j = 0; j < ptMatchers.length; j++) { 548 | if (ptMatchers[j].regex.test(name)) { 549 | markers.push({ label: name, coords: ll, icon: ptMatchers[j].icon, element: el[i] }); 550 | break; 551 | } 552 | } 553 | } 554 | 555 | this._info.length += dist_3d; 556 | 557 | _ = el[i].getElementsByTagNameNS('*', 'hr'); 558 | if (_.length > 0) { 559 | ll.meta.hr = parseInt(_[0].textContent); 560 | this._info.hr._points.push([this._info.length, ll.meta.hr]); 561 | this._info.hr._total += ll.meta.hr; 562 | } 563 | 564 | _ = el[i].getElementsByTagNameNS('*', 'cad'); 565 | if (_.length > 0) { 566 | ll.meta.cad = parseInt(_[0].textContent); 567 | this._info.cad._points.push([this._info.length, ll.meta.cad]); 568 | this._info.cad._total += ll.meta.cad; 569 | } 570 | 571 | _ = el[i].getElementsByTagNameNS('*', 'atemp'); 572 | if (_.length > 0) { 573 | ll.meta.atemp = parseInt(_[0].textContent); 574 | this._info.atemp._points.push([this._info.length, ll.meta.atemp]); 575 | this._info.atemp._total += ll.meta.atemp; 576 | } 577 | 578 | if (ll.meta.ele > this._info.elevation.max) { 579 | this._info.elevation.max = ll.meta.ele; 580 | } 581 | if (ll.meta.ele < this._info.elevation.min) { 582 | this._info.elevation.min = ll.meta.ele; 583 | } 584 | this._info.elevation._points.push([this._info.length, ll.meta.ele]); 585 | 586 | if (ll.meta.speed > this._info.speed.max) { 587 | this._info.speed.max = ll.meta.speed; 588 | } 589 | this._info.speed._points.push([this._info.length, ll.meta.speed]); 590 | 591 | if ((last == null) && (this._info.duration.start == null)) { 592 | this._info.duration.start = ll.meta.time; 593 | } 594 | this._info.duration.end = ll.meta.time; 595 | this._info.duration.total += time_diff; 596 | if (time_diff < options.max_point_interval) { 597 | this._info.duration.moving += time_diff; 598 | } 599 | 600 | if (ele_diff > 0) { 601 | this._info.elevation.gain += ele_diff; 602 | } else { 603 | this._info.elevation.loss += Math.abs(ele_diff); 604 | } 605 | 606 | last = ll; 607 | coords.push(ll); 608 | } 609 | 610 | // add track 611 | var l = new L.Polyline(coords, this._extract_styling(line, base_style, polyline_options)); 612 | this.fire('addline', { line: l, element: line }); 613 | layers.push(l); 614 | 615 | if (options.markers.startIcon) { 616 | // add start pin 617 | var marker = new L.Marker(coords[0], { 618 | clickable: options.marker_options.clickable, 619 | icon: options.markers.startIcon, 620 | }); 621 | this.fire('addpoint', { point: marker, point_type: 'start', element: el[0], line: line }); 622 | layers.push(marker); 623 | } 624 | 625 | if (options.markers.endIcon) { 626 | // add end pin 627 | var marker = new L.Marker(coords[coords.length-1], { 628 | clickable: options.marker_options.clickable, 629 | icon: options.markers.endIcon, 630 | }); 631 | this.fire('addpoint', { point: marker, point_type: 'end', element: el[el.length-1], line: line }); 632 | layers.push(marker); 633 | } 634 | 635 | // add named markers 636 | for (var i = 0; i < markers.length; i++) { 637 | var marker = new L.Marker(markers[i].coords, { 638 | clickable: options.marker_options.clickable, 639 | title: markers[i].label, 640 | icon: markers[i].icon 641 | }); 642 | this.fire('addpoint', { point: marker, point_type: 'label', element: markers[i].element }); 643 | layers.push(marker); 644 | } 645 | 646 | return layers; 647 | }, 648 | 649 | _get_polyline_options: function(polyline_options, i) { 650 | /* 651 | * Handle backwards compatibility with polyline_options being provided as a single object. 652 | * In this situation, the provided style is expected to apply to all routes and tracks in the file. 653 | */ 654 | if (! Array.isArray(polyline_options)) { 655 | return polyline_options; 656 | } 657 | return polyline_options[i] || {}; 658 | }, 659 | 660 | _extract_styling: function(el, base, overrides) { 661 | var style = this._merge_objs(_DEFAULT_POLYLINE_OPTS, base); 662 | var e = el.getElementsByTagNameNS(_GPX_STYLE_NS, 'line'); 663 | if (e.length > 0) { 664 | var _ = e[0].getElementsByTagName('color'); 665 | if (_.length > 0) style.color = '#' + _[0].textContent; 666 | var _ = e[0].getElementsByTagName('opacity'); 667 | if (_.length > 0) style.opacity = _[0].textContent; 668 | var _ = e[0].getElementsByTagName('weight'); 669 | if (_.length > 0) style.weight = _[0].textContent; 670 | var _ = e[0].getElementsByTagName('linecap'); 671 | if (_.length > 0) style.lineCap = _[0].textContent; 672 | var _ = e[0].getElementsByTagName('linejoin'); 673 | if (_.length > 0) style.lineJoin = _[0].textContent; 674 | var _ = e[0].getElementsByTagName('dasharray'); 675 | if (_.length > 0) style.dashArray = _[0].textContent; 676 | var _ = e[0].getElementsByTagName('dashoffset'); 677 | if (_.length > 0) style.dashOffset = _[0].textContent; 678 | } 679 | return this._merge_objs(style, overrides) 680 | }, 681 | 682 | _dist2d: function(a, b) { 683 | var R = 6371000; 684 | var dLat = this._deg2rad(b.lat - a.lat); 685 | var dLon = this._deg2rad(b.lng - a.lng); 686 | var r = Math.sin(dLat/2) * 687 | Math.sin(dLat/2) + 688 | Math.cos(this._deg2rad(a.lat)) * 689 | Math.cos(this._deg2rad(b.lat)) * 690 | Math.sin(dLon/2) * 691 | Math.sin(dLon/2); 692 | var c = 2 * Math.atan2(Math.sqrt(r), Math.sqrt(1-r)); 693 | var d = R * c; 694 | return d; 695 | }, 696 | 697 | _dist3d: function(a, b) { 698 | var planar = this._dist2d(a, b); 699 | var height = Math.abs(b.meta.ele - a.meta.ele); 700 | return Math.sqrt(Math.pow(planar, 2) + Math.pow(height, 2)); 701 | }, 702 | 703 | _deg2rad: function(deg) { 704 | return deg * Math.PI / 180; 705 | } 706 | }); 707 | -------------------------------------------------------------------------------- /icons/pin-icon-end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpetazzoni/leaflet-gpx/53729493fb20d8987dd0990346a5a19ff297a31d/icons/pin-icon-end.png -------------------------------------------------------------------------------- /icons/pin-icon-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpetazzoni/leaflet-gpx/53729493fb20d8987dd0990346a5a19ff297a31d/icons/pin-icon-start.png -------------------------------------------------------------------------------- /icons/pin-icon-wpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpetazzoni/leaflet-gpx/53729493fb20d8987dd0990346a5a19ff297a31d/icons/pin-icon-wpt.png -------------------------------------------------------------------------------- /icons/pin-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpetazzoni/leaflet-gpx/53729493fb20d8987dd0990346a5a19ff297a31d/icons/pin-shadow.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-gpx", 3 | "version": "2.2.0", 4 | "description": "A Leaflet plugin for showing a GPX track on a map", 5 | "main": "gpx.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mpetazzoni/leaflet-gpx.git" 12 | }, 13 | "keywords": [ 14 | "leaflet", 15 | "gpx", 16 | "leaflet-gpx", 17 | "map", 18 | "gps" 19 | ], 20 | "author": "Maxime Petazzoni (https://www.bulix.org)", 21 | "license": "BSD-2-Clause", 22 | "bugs": { 23 | "url": "https://github.com/mpetazzoni/leaflet-gpx/issues" 24 | }, 25 | "homepage": "https://github.com/mpetazzoni/leaflet-gpx#readme" 26 | } 27 | --------------------------------------------------------------------------------