Initial commit
This commit is contained in:
commit
b7d90b9afe
16245 files changed
+3396666
No files matched your search
Executable
+24
@@ -0,0 +1,24 @@
|
||||
Copyright (C) 2011-2012 Pavel Shramov
|
||||
Copyright (C) 2013 Maxime Petazzoni <maxime.petazzoni@bulix.org>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
# GPX plugin for Leaflet
|
||||
|
||||
[](https://cdnjs.com/libraries/leaflet-gpx)
|
||||
|
||||
[Leaflet](http://www.leafletjs.com) is a Javascript library for displaying
|
||||
interactive maps. This plugin, based on the work of [Pavel
|
||||
Shramov](http://github.com/shramov) and his
|
||||
[leaflet-plugins](http://github.com/shramov/leaflet-plugins), it allows for the
|
||||
analysis and parsing of a GPX track in order to display it as a Leaflet map
|
||||
layer. As it parses the GPX data, it will record information about the recorded
|
||||
track, including total time, moving time, total distance, elevation stats and
|
||||
heart-rate.
|
||||
|
||||
GPX parsing will automatically handle pauses in the track with a default
|
||||
tolerance interval of 15 seconds between points. You can configure this
|
||||
interval by setting `max_point_interval`, in milliseconds, in the options
|
||||
passed to the `GPX` constructor.
|
||||
|
||||
I've put together a complete example as a
|
||||
[demo](http://mpetazzoni.github.com/leaflet-gpx/).
|
||||
|
||||
## License
|
||||
|
||||
`leaflet-gpx` is under the *BSD 2-clause license*. Please refer to the
|
||||
attached LICENSE file and/or to the copyright header in gpx.js for more
|
||||
information.
|
||||
|
||||
## Usage
|
||||
|
||||
Usage is very simple. First, include the Leaflet.js and Leaflet-GPX
|
||||
scripts in your HTML page:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.css" />
|
||||
<!-- ... -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-gpx/1.4.0/gpx.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Now, let's consider we have a Leaflet map:
|
||||
|
||||
```javascript
|
||||
var map = L.map('map');
|
||||
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: 'Map data © <a href="http://www.osm.org">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
Displaying a GPX track on it is as easy as:
|
||||
|
||||
```javascript
|
||||
var gpx = '...'; // URL to your GPX file or the GPX itself
|
||||
new L.GPX(gpx, {async: true}).on('loaded', function(e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
Some GPX tracks contain the actual route/track twice, both the `<trk>` and
|
||||
`<rte>` elements are used. You can tell `leaflet-gpx` which tag to use or to
|
||||
use both (which is the default setting for backwards compatibility) with the
|
||||
`gpx_options` object in the second argument of the constructor. The member
|
||||
`parseElements` controls this behavior, it should be an array that contains
|
||||
`'route'` and/or `'track'`.
|
||||
|
||||
### Available functions
|
||||
|
||||
If you want to display additional information about the GPX track, you can do
|
||||
so in the 'loaded' event handler, calling one of the following methods on the
|
||||
`GPX` object `e.target`:
|
||||
|
||||
* `get_name()`: returns the name of the GPX track
|
||||
* `get_distance()`: returns the total track distance, in meters
|
||||
* `get_start_time()`: returns a Javascript `Date` object representing the
|
||||
starting time
|
||||
* `get_end_time()`: returns a Javascript `Date` object representing when the
|
||||
last point was recorded
|
||||
* `get_moving_time()`: returns the moving time, in milliseconds
|
||||
* `get_total_time()`: returns the total track time, in milliseconds
|
||||
* `get_moving_pace()`: returns the average moving pace in milliseconds per km
|
||||
* `get_moving_speed()`: returns the average moving speed in km per hour
|
||||
* `get_total_speed()`: returns the average total speed in km per hour
|
||||
* `get_elevation_min()`: returns the lowest elevation, in meters
|
||||
* `get_elevation_max()`: returns the highest elevation, in meters
|
||||
* `get_elevation_gain()`: returns the cumulative elevation gain, in meters
|
||||
* `get_elevation_loss()`: returns the cumulative elevation loss, in meters
|
||||
* `get_average_hr()`: returns the average heart rate (if available)
|
||||
* `get_average_cadence()`: returns the average cadence (if available)
|
||||
* `get_average_temp()`: returns the average of the temperature (if available)
|
||||
|
||||
If you're not a fan of the metric system, you also have the following methods
|
||||
at your disposal:
|
||||
|
||||
* `get_distance_imp()`: returns the total track distance in miles
|
||||
* `get_moving_pace_imp()`: returns the average moving pace in milliseconds per
|
||||
hour
|
||||
* `get_moving_speed_imp()`: returns the average moving speed in miles per hour
|
||||
* `get_total_speed_imp()`: returns the average total speed in miles per hour
|
||||
* `get_elevation_min_imp()`: returns the lowest elevation, in feet
|
||||
* `get_elevation_max_imp()`: returns the highest elevation, in feet
|
||||
* `get_elevation_gain_imp()`: returns the cumulative elevation gain, in feet
|
||||
* `get_elevation_loss_imp()`: returns the cumulative elevation loss, in feet
|
||||
|
||||
The reason why these methods return milliseconds is that you have at your
|
||||
disposal nice helper methods to format a duration in milliseconds into a cool
|
||||
string:
|
||||
|
||||
* `get_duration_string(duration, hidems)` format to a string like `3:07'48"`
|
||||
or `59'32.431`, where `duration` is in
|
||||
milliseconds and `hidems` is an optional boolean you can use to request never
|
||||
to display millisecond precision.
|
||||
* `get_duration_string_iso(duration, hidems)` formats to an ISO like
|
||||
representation like `3:07:48` or `59:32.431`, where `duration` is in
|
||||
milliseconds and `hidems` is an optional boolean you can use to request never
|
||||
to display millisecond precision.
|
||||
|
||||
You can also get full elevation, heartrate, cadence and temperature data with:
|
||||
|
||||
* `get_elevation_data()` and `get_elevation_data_imp()`
|
||||
* `get_heartrate_data()` and `get_heartrate_data_imp()`
|
||||
* `get_cadence_data()` and `get_cadence_data_imp()`
|
||||
* `get_temp_data()` and `get_temp_data_imp()`
|
||||
|
||||
These methods all return an array of points `[distance, value, tooltip]` where
|
||||
the distance is either in kilometers or in miles and the elevation in meters or
|
||||
feet, depending on whether you use the `_imp` variant or not. Heart rate,
|
||||
obviously, doesn't change.
|
||||
|
||||
### Reloading
|
||||
|
||||
You can make `leaflet-gpx` reload the source GPX file by calling the
|
||||
`reload()` method. For example, to trigger a reload every 5 seconds, you
|
||||
can do:
|
||||
|
||||
```javascript
|
||||
var gpx = new L.GPX(gpxFile);
|
||||
setInterval(function() {
|
||||
gpx.reload();
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
## About marker icons
|
||||
|
||||
By default `gpx.js` will use `pin-icon-start.png`, `pin-icon-end.png` and
|
||||
`pin-shadow.png` as the marker icons URLs for, respectively, the start marker,
|
||||
the end marker and their drop shadow. Since it might not be convenient that
|
||||
these images have to reside under the same directory as your HTML page, it is
|
||||
possible to override the marker icon URLs and sizes by passing a
|
||||
`marker_options` object to the `GPX` options object.
|
||||
|
||||
The field names are the same as for custom Leaflet icons, as explained in the
|
||||
[Markers with custom icons](http://leafletjs.com/examples/custom-icons.html)
|
||||
page in Leaflet's documentation. The only difference is that instead of
|
||||
`iconUrl` you should specify `startIconUrl` and `endIconUrl` for the start and
|
||||
end markers, respectively.
|
||||
|
||||
Note that you do not need to override all the marker icon options as `gpx.js`
|
||||
will use sensible defaults with sizes matching the provided icon images. Here
|
||||
is how you would override the URL of the provided icons if you decided to place
|
||||
them in an `images/` directory:
|
||||
|
||||
```javascript
|
||||
var url = '...'; // URL to your GPX file
|
||||
new L.GPX(url, {
|
||||
async: true,
|
||||
marker_options: {
|
||||
startIconUrl: 'images/pin-icon-start.png',
|
||||
endIconUrl: 'images/pin-icon-end.png',
|
||||
shadowUrl: 'images/pin-shadow.png'
|
||||
}
|
||||
}).on('loaded', function(e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
## About waypoints
|
||||
|
||||
By default `gpx.js` will parse Waypoints from a GPX file. This may also
|
||||
be steered via the value `waypoint` in `gpx_options`, e.g.
|
||||
`parseElements: ['track', 'route', 'waypoint']`.
|
||||
|
||||
Waypoint icons: by default the `pin-icon-wpt.png` icon is shown for each
|
||||
waypoint. This can be overridden by setting `marker_options.wptIconUrls`
|
||||
in the `L.GPX` constructor options, as a mapping from the waypoint "SYM"
|
||||
name to a user-supplied icon file or URL. The empty string `''` is used
|
||||
by the waypoint tag does not define a "SYM" name. See the example below:
|
||||
|
||||
```javascript
|
||||
new L.GPX(app.params.gpx_url, {
|
||||
async: true,
|
||||
marker_options: {
|
||||
wptIconUrls: {
|
||||
'': 'img/gpx/default-waypoint.png',
|
||||
'Geocache Found': 'img/gpx/geocache.png',
|
||||
'Park': 'img/gpx/tree.png'
|
||||
},
|
||||
...
|
||||
shadowUrl: 'http://github.com/mpetazzoni/leaflet-gpx/raw/master/pin-shadow.png'
|
||||
}
|
||||
}).on('loaded', function (e) {
|
||||
var gpx = e.target;
|
||||
map.fitBounds(gpx.getBounds());
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
## Custom markers
|
||||
|
||||
You can also use your own icons/markers if you want to use custom
|
||||
markers, for example from `leaflet-awesome-markers`. To specify you own
|
||||
markers, set `startIcon`, `endIcon`, and a map of `wptIcons` by waypoint
|
||||
symbol (see above). Those should be marker icon objects usable by
|
||||
Leaflet as the `icon` property of a `L.Marker` object.
|
||||
|
||||
```javascript
|
||||
new L.GPX(app.params.gpx_url, {
|
||||
async: true,
|
||||
marker_options: {
|
||||
wptIcons: {
|
||||
'Coffee shop': new L.AwesomeMarkers.icon({
|
||||
icon: 'coffee',
|
||||
prefix: 'fa',
|
||||
markerColor: 'blue',
|
||||
iconColor: 'white'
|
||||
})
|
||||
}
|
||||
}
|
||||
}).on('loaded', function (e) {
|
||||
var gpx = e.target;
|
||||
map.fitBounds(gpx.getBounds());
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
## Named points
|
||||
|
||||
GPX points can be named, for example to denote certain POIs (points of
|
||||
interest). You can setup rules to match point names to create labeled
|
||||
markers for those points by providing a `pointMatchers` array in the
|
||||
`marker_options`. Each element in this array must define a `regex` to
|
||||
match the point's name and an `icon` object (any `L.Marker` or
|
||||
for example an `L.AwesomeMarkers.icon` as shown above in _Custom
|
||||
markers_).
|
||||
|
||||
Each named point in the GPX track is evaluated against those rules and
|
||||
a marker is created with the point's name as label from the first
|
||||
matching rule.
|
||||
|
||||
```javascript
|
||||
new L.GPX(app.params.gpx_url, {
|
||||
async: true,
|
||||
marker_options: {
|
||||
pointMatchers: [
|
||||
{
|
||||
regex: /Coffee/,
|
||||
icon: new L.AwesomeMarkers.icon({
|
||||
icon: 'coffee',
|
||||
markerColor: 'blue',
|
||||
iconColor: 'white'
|
||||
}),
|
||||
},
|
||||
{
|
||||
regex: /Home/,
|
||||
icon: new L.AwesomeMarkers.icon({
|
||||
icon: 'home',
|
||||
markerColor: 'green',
|
||||
iconColor: 'white'
|
||||
}),
|
||||
}
|
||||
]
|
||||
}
|
||||
}).on('loaded', function(e) {
|
||||
var gpx = e.target;
|
||||
map.fitToBounds(gpx.getBounds());
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
Events are fired on the `L.GPX` object as the GPX data is being parsed
|
||||
and the map layers generated. You can listen for those events by
|
||||
attaching the corresponding event listener on the `L.GPX` object:
|
||||
|
||||
```javascript
|
||||
new L.GPX(app.params.gpx_url, {
|
||||
// options
|
||||
}).on('addpoint', function(e) {
|
||||
console.log('Added ' + e.point_type + ' point: ' + e.point);
|
||||
}).on('loaded', function(e) {
|
||||
var gpx = e.target;
|
||||
map.fitToBounds(gpx.getBounds());
|
||||
}).on('error', function(e) {
|
||||
console.log('Error loading file: ' + e.err);
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
`addpoint` events are fired for every marker added to the map, in
|
||||
particular for the start and end points, all the waypoints, and all the
|
||||
named points that matched `pointMatchers` rules. Each `addpoint` event
|
||||
contains the following properties:
|
||||
|
||||
- `point`: the marker object itself, from which you can get or modify
|
||||
the latitude and longitude of the point and any other attribute of the
|
||||
marker.
|
||||
- `point_type`: one of `start`, `end`, `waypoint` or `label`, allowing
|
||||
you to identify what type of point the marker is for.
|
||||
- `element`: the track point element the marker was created for.
|
||||
|
||||
One use case for those events is for example to attach additional
|
||||
content or behavior to the markers that were generated (popups, etc).
|
||||
|
||||
`error` events are fired when no layers of the type(s) specified in
|
||||
`options.gpx_options.parseElements` can be parsed out of the given
|
||||
file. For instance, `error` would be fired if a file with no waypoints
|
||||
was attempted to be loaded with `parseElements` set to `['waypoint']`.
|
||||
Each `error` event contains the following property:
|
||||
|
||||
- `err`: a message with details about the error that occurred.
|
||||
|
||||
## Line styling
|
||||
|
||||
`leaflet-gpx` understands the [GPX
|
||||
Style](http://www.topografix.com/GPX/gpx_style/0/2) extension, and will
|
||||
extract styling information defined on routes and track segments to use
|
||||
for drawing the corresponding polyline.
|
||||
|
||||
```xml
|
||||
<trkseg>
|
||||
<extensions>
|
||||
<line xmlns="http://www.topografix.com/GPX/gpx_style/0/2">
|
||||
<color>FF0000</color>
|
||||
<opacity>0.5</opacity>
|
||||
<weight>1</weight>
|
||||
<linecap>square</linecap>
|
||||
</line>
|
||||
</extensions>
|
||||
<trkpt lat="..." lon="..."></trkpt>
|
||||
</trkseg>
|
||||
```
|
||||
|
||||
You can override the style of the lines by passing a `polyline_options`
|
||||
object into the `options` argument of the `L.GPX` constructor:
|
||||
|
||||
```javascript
|
||||
new L.GPX(url, {
|
||||
polyline_options: {
|
||||
color: 'green',
|
||||
opacity: 0.75,
|
||||
weight: 3,
|
||||
lineCap: 'round'
|
||||
}
|
||||
}).on('loaded', function(e) {
|
||||
var gpx = e.target;
|
||||
map.fitToBounds(gpx.getBounds());
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
For more information on the available polyline styling options, refer to
|
||||
the [Leaflet documentation on
|
||||
Polyline](https://leafletjs.com/reference-1.3.0.html#polyline). By
|
||||
default, if no styling is available, the line will be drawn in _blue_.
|
||||
|
||||
## GPX parsing options
|
||||
|
||||
### Multiple track segments within each track
|
||||
|
||||
GPX file may contain multiple tracks represented by `<trk>` elements,
|
||||
each track possibly composed of multiple segments with `<trkseg>`
|
||||
elements. Although this plugin will always represent each GPX route and
|
||||
each GPX track as distinct entities with their own start and end
|
||||
markers, track segments will by default be joined into a single line.
|
||||
|
||||
You can disable this behavior by setting the `joinTrackSegments` flag to
|
||||
`false` in the `gpx_options`:
|
||||
|
||||
```javascript
|
||||
new L.GPX(url, {
|
||||
gpx_options: {
|
||||
joinTrackSegments: false
|
||||
}
|
||||
}).on('loaded', function(e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}).addTo(map);
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
* Distance calculation is relatively accurate, but elevation change
|
||||
calculation is not topographically adjusted, so the total elevation
|
||||
gain/loss/change might appear inaccurate in some situations.
|
||||
* Currently doesn't seem to work in IE8/9. See #9 and #11 for
|
||||
discussion.
|
||||
Executable
+600
@@ -0,0 +1,600 @@
|
||||
/**
|
||||
* Copyright (C) 2011-2012 Pavel Shramov
|
||||
* Copyright (C) 2013-2017 Maxime Petazzoni <maxime.petazzoni@bulix.org>
|
||||
* All Rights Reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* - Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Thanks to Pavel Shramov who provided the initial implementation and Leaflet
|
||||
* integration. Original code was at https://github.com/shramov/leaflet-plugins.
|
||||
*
|
||||
* It was then cleaned-up and modified to record and make available more
|
||||
* information about the GPX track while it is being parsed so that the result
|
||||
* can be used to display additional information about the track that is
|
||||
* rendered on the Leaflet map.
|
||||
*/
|
||||
|
||||
var L = L || require('leaflet');
|
||||
|
||||
var _MAX_POINT_INTERVAL_MS = 15000;
|
||||
var _SECOND_IN_MILLIS = 1000;
|
||||
var _MINUTE_IN_MILLIS = 60 * _SECOND_IN_MILLIS;
|
||||
var _HOUR_IN_MILLIS = 60 * _MINUTE_IN_MILLIS;
|
||||
var _DAY_IN_MILLIS = 24 * _HOUR_IN_MILLIS;
|
||||
|
||||
var _GPX_STYLE_NS = 'http://www.topografix.com/GPX/gpx_style/0/2';
|
||||
|
||||
var _DEFAULT_MARKER_OPTS = {
|
||||
startIconUrl: 'pin-icon-start.png',
|
||||
endIconUrl: 'pin-icon-end.png',
|
||||
shadowUrl: 'pin-shadow.png',
|
||||
wptIcons: [],
|
||||
wptIconUrls : {
|
||||
'': 'pin-icon-wpt.png',
|
||||
},
|
||||
pointMatchers: [],
|
||||
iconSize: [33, 50],
|
||||
shadowSize: [50, 50],
|
||||
iconAnchor: [16, 45],
|
||||
shadowAnchor: [16, 47],
|
||||
clickable: false
|
||||
};
|
||||
var _DEFAULT_POLYLINE_OPTS = {
|
||||
color: 'blue'
|
||||
};
|
||||
var _DEFAULT_GPX_OPTS = {
|
||||
parseElements: ['track', 'route', 'waypoint'],
|
||||
joinTrackSegments: true
|
||||
};
|
||||
|
||||
L.GPX = L.FeatureGroup.extend({
|
||||
initialize: function(gpx, options) {
|
||||
options.max_point_interval = options.max_point_interval || _MAX_POINT_INTERVAL_MS;
|
||||
options.marker_options = this._merge_objs(
|
||||
_DEFAULT_MARKER_OPTS,
|
||||
options.marker_options || {});
|
||||
options.polyline_options = options.polyline_options || {};
|
||||
options.gpx_options = this._merge_objs(
|
||||
_DEFAULT_GPX_OPTS,
|
||||
options.gpx_options || {});
|
||||
|
||||
L.Util.setOptions(this, options);
|
||||
|
||||
// Base icon class for track pins.
|
||||
L.GPXTrackIcon = L.Icon.extend({ options: options.marker_options });
|
||||
|
||||
this._gpx = gpx;
|
||||
this._layers = {};
|
||||
this._init_info();
|
||||
|
||||
if (gpx) {
|
||||
this._parse(gpx, options, this.options.async);
|
||||
}
|
||||
},
|
||||
|
||||
get_duration_string: function(duration, hidems) {
|
||||
var s = '';
|
||||
|
||||
if (duration >= _DAY_IN_MILLIS) {
|
||||
s += Math.floor(duration / _DAY_IN_MILLIS) + 'd ';
|
||||
duration = duration % _DAY_IN_MILLIS;
|
||||
}
|
||||
|
||||
if (duration >= _HOUR_IN_MILLIS) {
|
||||
s += Math.floor(duration / _HOUR_IN_MILLIS) + ':';
|
||||
duration = duration % _HOUR_IN_MILLIS;
|
||||
}
|
||||
|
||||
var mins = Math.floor(duration / _MINUTE_IN_MILLIS);
|
||||
duration = duration % _MINUTE_IN_MILLIS;
|
||||
if (mins < 10) s += '0';
|
||||
s += mins + '\'';
|
||||
|
||||
var secs = Math.floor(duration / _SECOND_IN_MILLIS);
|
||||
duration = duration % _SECOND_IN_MILLIS;
|
||||
if (secs < 10) s += '0';
|
||||
s += secs;
|
||||
|
||||
if (!hidems && duration > 0) s += '.' + Math.round(Math.floor(duration)*1000)/1000;
|
||||
else s += '"';
|
||||
|
||||
return s;
|
||||
},
|
||||
|
||||
get_duration_string_iso: function(duration, hidems) {
|
||||
var s = this.get_duration_string(duration, hidems);
|
||||
return s.replace("'",':').replace('"','');
|
||||
},
|
||||
|
||||
// Public methods
|
||||
to_miles: function(v) { return v / 1.60934; },
|
||||
to_ft: function(v) { return v * 3.28084; },
|
||||
m_to_km: function(v) { return v / 1000; },
|
||||
m_to_mi: function(v) { return v / 1609.34; },
|
||||
|
||||
get_name: function() { return this._info.name; },
|
||||
get_desc: function() { return this._info.desc; },
|
||||
get_author: function() { return this._info.author; },
|
||||
get_copyright: function() { return this._info.copyright; },
|
||||
get_distance: function() { return this._info.length; },
|
||||
get_distance_imp: function() { return this.to_miles(this.m_to_km(this.get_distance())); },
|
||||
|
||||
get_start_time: function() { return this._info.duration.start; },
|
||||
get_end_time: function() { return this._info.duration.end; },
|
||||
get_moving_time: function() { return this._info.duration.moving; },
|
||||
get_total_time: function() { return this._info.duration.total; },
|
||||
|
||||
get_moving_pace: function() { return this.get_moving_time() / this.m_to_km(this.get_distance()); },
|
||||
get_moving_pace_imp: function() { return this.get_moving_time() / this.get_distance_imp(); },
|
||||
|
||||
get_moving_speed: function() { return this.m_to_km(this.get_distance()) / (this.get_moving_time() / (3600 * 1000)) ; },
|
||||
get_moving_speed_imp:function() { return this.to_miles(this.m_to_km(this.get_distance())) / (this.get_moving_time() / (3600 * 1000)) ; },
|
||||
|
||||
get_total_speed: function() { return this.m_to_km(this.get_distance()) / (this.get_total_time() / (3600 * 1000)); },
|
||||
get_total_speed_imp: function() { return this.to_miles(this.m_to_km(this.get_distance())) / (this.get_total_time() / (3600 * 1000)); },
|
||||
|
||||
get_elevation_gain: function() { return this._info.elevation.gain; },
|
||||
get_elevation_loss: function() { return this._info.elevation.loss; },
|
||||
get_elevation_gain_imp: function() { return this.to_ft(this.get_elevation_gain()); },
|
||||
get_elevation_loss_imp: function() { return this.to_ft(this.get_elevation_loss()); },
|
||||
get_elevation_data: function() {
|
||||
var _this = this;
|
||||
return this._info.elevation._points.map(
|
||||
function(p) { return _this._prepare_data_point(p, _this.m_to_km, null,
|
||||
function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' m'; });
|
||||
});
|
||||
},
|
||||
get_elevation_data_imp: function() {
|
||||
var _this = this;
|
||||
return this._info.elevation._points.map(
|
||||
function(p) { return _this._prepare_data_point(p, _this.m_to_mi, _this.to_ft,
|
||||
function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' ft'; });
|
||||
});
|
||||
},
|
||||
get_elevation_max: function() { return this._info.elevation.max; },
|
||||
get_elevation_min: function() { return this._info.elevation.min; },
|
||||
get_elevation_max_imp: function() { return this.to_ft(this.get_elevation_max()); },
|
||||
get_elevation_min_imp: function() { return this.to_ft(this.get_elevation_min()); },
|
||||
|
||||
get_average_hr: function() { return this._info.hr.avg; },
|
||||
get_average_temp: function() { return this._info.atemp.avg; },
|
||||
get_average_cadence: function() { return this._info.cad.avg; },
|
||||
get_heartrate_data: function() {
|
||||
var _this = this;
|
||||
return this._info.hr._points.map(
|
||||
function(p) { return _this._prepare_data_point(p, _this.m_to_km, null,
|
||||
function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' bpm'; });
|
||||
});
|
||||
},
|
||||
get_heartrate_data_imp: function() {
|
||||
var _this = this;
|
||||
return this._info.hr._points.map(
|
||||
function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null,
|
||||
function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' bpm'; });
|
||||
});
|
||||
},
|
||||
get_cadence_data: function() {
|
||||
var _this = this;
|
||||
return this._info.cad._points.map(
|
||||
function(p) { return _this._prepare_data_point(p, _this.m_to_km, null,
|
||||
function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' rpm'; });
|
||||
});
|
||||
},
|
||||
get_temp_data: function() {
|
||||
var _this = this;
|
||||
return this._info.atemp._points.map(
|
||||
function(p) { return _this._prepare_data_point(p, _this.m_to_km, null,
|
||||
function(a, b) { return a.toFixed(2) + ' km, ' + b.toFixed(0) + ' degrees'; });
|
||||
});
|
||||
},
|
||||
get_cadence_data_imp: function() {
|
||||
var _this = this;
|
||||
return this._info.cad._points.map(
|
||||
function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null,
|
||||
function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' rpm'; });
|
||||
});
|
||||
},
|
||||
get_temp_data_imp: function() {
|
||||
var _this = this;
|
||||
return this._info.atemp._points.map(
|
||||
function(p) { return _this._prepare_data_point(p, _this.m_to_mi, null,
|
||||
function(a, b) { return a.toFixed(2) + ' mi, ' + b.toFixed(0) + ' degrees'; });
|
||||
});
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
this._init_info();
|
||||
this.clearLayers();
|
||||
this._parse(this._gpx, this.options, this.options.async);
|
||||
},
|
||||
|
||||
// Private methods
|
||||
_merge_objs: function(a, b) {
|
||||
var _ = {};
|
||||
for (var attr in a) { _[attr] = a[attr]; }
|
||||
for (var attr in b) { _[attr] = b[attr]; }
|
||||
return _;
|
||||
},
|
||||
|
||||
_prepare_data_point: function(p, trans1, trans2, trans_tooltip) {
|
||||
var r = [trans1 && trans1(p[0]) || p[0], trans2 && trans2(p[1]) || p[1]];
|
||||
r.push(trans_tooltip && trans_tooltip(r[0], r[1]) || (r[0] + ': ' + r[1]));
|
||||
return r;
|
||||
},
|
||||
|
||||
_init_info: function() {
|
||||
this._info = {
|
||||
name: null,
|
||||
length: 0.0,
|
||||
elevation: {gain: 0.0, loss: 0.0, max: 0.0, min: Infinity, _points: []},
|
||||
hr: {avg: 0, _total: 0, _points: []},
|
||||
duration: {start: null, end: null, moving: 0, total: 0},
|
||||
atemp: {avg: 0, _total: 0, _points: []},
|
||||
cad: {avg: 0, _total: 0, _points: []}
|
||||
};
|
||||
},
|
||||
|
||||
_load_xml: function(url, cb, options, async) {
|
||||
if (async == undefined) async = this.options.async;
|
||||
if (options == undefined) options = this.options;
|
||||
|
||||
var req = new window.XMLHttpRequest();
|
||||
req.open('GET', url, async);
|
||||
try {
|
||||
req.overrideMimeType('text/xml'); // unsupported by IE
|
||||
} catch(e) {}
|
||||
req.onreadystatechange = function() {
|
||||
if (req.readyState != 4) return;
|
||||
if(req.status == 200) cb(req.responseXML, options);
|
||||
};
|
||||
req.send(null);
|
||||
},
|
||||
|
||||
_parse: function(input, options, async) {
|
||||
var _this = this;
|
||||
var cb = function(gpx, options) {
|
||||
var layers = _this._parse_gpx_data(gpx, options);
|
||||
if (!layers) {
|
||||
_this.fire('error', { err: 'No parseable layers of type(s) ' + JSON.stringify(options.gpx_options.parseElements) });
|
||||
return;
|
||||
}
|
||||
_this.addLayer(layers);
|
||||
_this.fire('loaded', { layers: layers, element: gpx });
|
||||
}
|
||||
if (input.substr(0,1)==='<') { // direct XML has to start with a <
|
||||
var parser = new DOMParser();
|
||||
if (async) {
|
||||
setTimeout(function() {
|
||||
cb(parser.parseFromString(input, "text/xml"), options);
|
||||
});
|
||||
} else {
|
||||
cb(parser.parseFromString(input, "text/xml"), options);
|
||||
}
|
||||
} else {
|
||||
this._load_xml(input, cb, options, async);
|
||||
}
|
||||
},
|
||||
|
||||
_parse_gpx_data: function(xml, options) {
|
||||
var i, t, l, el, layers = [];
|
||||
|
||||
var name = xml.getElementsByTagName('name');
|
||||
if (name.length > 0) {
|
||||
this._info.name = name[0].textContent;
|
||||
}
|
||||
var desc = xml.getElementsByTagName('desc');
|
||||
if (desc.length > 0) {
|
||||
this._info.desc = desc[0].textContent;
|
||||
}
|
||||
var author = xml.getElementsByTagName('author');
|
||||
if (author.length > 0) {
|
||||
this._info.author = author[0].textContent;
|
||||
}
|
||||
var copyright = xml.getElementsByTagName('copyright');
|
||||
if (copyright.length > 0) {
|
||||
this._info.copyright = copyright[0].textContent;
|
||||
}
|
||||
|
||||
var parseElements = options.gpx_options.parseElements;
|
||||
if (parseElements.indexOf('route') > -1) {
|
||||
// routes are <rtept> tags inside <rte> sections
|
||||
var routes = xml.getElementsByTagName('rte');
|
||||
for (i = 0; i < routes.length; i++) {
|
||||
layers = layers.concat(this._parse_segment(routes[i], options, {}, 'rtept'));
|
||||
}
|
||||
}
|
||||
|
||||
if (parseElements.indexOf('track') > -1) {
|
||||
// tracks are <trkpt> tags in one or more <trkseg> sections in each <trk>
|
||||
var tracks = xml.getElementsByTagName('trk');
|
||||
for (i = 0; i < tracks.length; i++) {
|
||||
var track = tracks[i];
|
||||
var polyline_options = this._extract_styling(track);
|
||||
|
||||
if (options.gpx_options.joinTrackSegments) {
|
||||
layers = layers.concat(this._parse_segment(track, options, polyline_options, 'trkpt'));
|
||||
} else {
|
||||
var segments = track.getElementsByTagName('trkseg');
|
||||
for (j = 0; j < segments.length; j++) {
|
||||
layers = layers.concat(this._parse_segment(segments[j], options, polyline_options, 'trkpt'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._info.hr.avg = Math.round(this._info.hr._total / this._info.hr._points.length);
|
||||
this._info.cad.avg = Math.round(this._info.cad._total / this._info.cad._points.length);
|
||||
this._info.atemp.avg = Math.round(this._info.atemp._total / this._info.atemp._points.length);
|
||||
|
||||
// parse waypoints and add markers for each of them
|
||||
if (parseElements.indexOf('waypoint') > -1) {
|
||||
el = xml.getElementsByTagName('wpt');
|
||||
for (i = 0; i < el.length; i++) {
|
||||
var ll = new L.LatLng(
|
||||
el[i].getAttribute('lat'),
|
||||
el[i].getAttribute('lon'));
|
||||
|
||||
var nameEl = el[i].getElementsByTagName('name');
|
||||
var name = '';
|
||||
if (nameEl.length > 0) {
|
||||
name = nameEl[0].textContent;
|
||||
}
|
||||
|
||||
var descEl = el[i].getElementsByTagName('desc');
|
||||
var desc = '';
|
||||
if (descEl.length > 0) {
|
||||
desc = descEl[0].textContent;
|
||||
}
|
||||
|
||||
var symEl = el[i].getElementsByTagName('sym');
|
||||
var symKey = '';
|
||||
if (symEl.length > 0) {
|
||||
symKey = symEl[0].textContent;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add waypoint marker based on the waypoint symbol key.
|
||||
*
|
||||
* First look for a configured icon for that symKey. If not found, look
|
||||
* for a configured icon URL for that symKey and build an icon from it.
|
||||
* Otherwise, fall back to the default icon if one was configured, or
|
||||
* finally to the default icon URL.
|
||||
*/
|
||||
var wptIcons = options.marker_options.wptIcons;
|
||||
var wptIconUrls = options.marker_options.wptIconUrls;
|
||||
var symIcon;
|
||||
if (wptIcons && wptIcons[symKey]) {
|
||||
symIcon = wptIcons[symKey];
|
||||
} else if (wptIconUrls && wptIconUrls[symKey]) {
|
||||
symIcon = new L.GPXTrackIcon({iconUrl: wptIconUrls[symKey]});
|
||||
} else if (wptIcons && wptIcons['']) {
|
||||
symIcon = wptIcons[''];
|
||||
} else if (wptIconUrls && wptIconUrls['']) {
|
||||
symIcon = new L.GPXTrackIcon({iconUrl: wptIconUrls['']});
|
||||
} else {
|
||||
console.log('No icon or icon URL configured for symbol type "' + symKey
|
||||
+ '", and no fallback configured; ignoring waypoint.');
|
||||
continue;
|
||||
}
|
||||
|
||||
var marker = new L.Marker(ll, {
|
||||
clickable: options.marker_options.clickable,
|
||||
title: name,
|
||||
icon: symIcon
|
||||
});
|
||||
marker.bindPopup("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : '')).openPopup();
|
||||
this.fire('addpoint', { point: marker, point_type: 'waypoint', element: el[i] });
|
||||
layers.push(marker);
|
||||
}
|
||||
}
|
||||
|
||||
if (layers.length > 1) {
|
||||
return new L.FeatureGroup(layers);
|
||||
} else if (layers.length == 1) {
|
||||
return layers[0];
|
||||
}
|
||||
},
|
||||
|
||||
_parse_segment: function(line, options, polyline_options, tag) {
|
||||
var el = line.getElementsByTagName(tag);
|
||||
if (!el.length) return [];
|
||||
|
||||
var coords = [];
|
||||
var markers = [];
|
||||
var layers = [];
|
||||
var last = null;
|
||||
|
||||
for (var i = 0; i < el.length; i++) {
|
||||
var _, ll = new L.LatLng(
|
||||
el[i].getAttribute('lat'),
|
||||
el[i].getAttribute('lon'));
|
||||
ll.meta = { time: null, ele: null, hr: null, cad: null, atemp: null };
|
||||
|
||||
_ = el[i].getElementsByTagName('time');
|
||||
if (_.length > 0) {
|
||||
ll.meta.time = new Date(Date.parse(_[0].textContent));
|
||||
} else {
|
||||
ll.meta.time = new Date('1970-01-01T00:00:00');
|
||||
}
|
||||
|
||||
_ = el[i].getElementsByTagName('ele');
|
||||
if (_.length > 0) {
|
||||
ll.meta.ele = parseFloat(_[0].textContent);
|
||||
}
|
||||
|
||||
_ = el[i].getElementsByTagName('name');
|
||||
if (_.length > 0) {
|
||||
var name = _[0].textContent;
|
||||
var ptMatchers = options.marker_options.pointMatchers || [];
|
||||
|
||||
for (var j = 0; j < ptMatchers.length; j++) {
|
||||
if (ptMatchers[j].regex.test(name)) {
|
||||
markers.push({ label: name, coords: ll, icon: ptMatchers[j].icon, element: el[i] });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = el[i].getElementsByTagNameNS('*', 'hr');
|
||||
if (_.length > 0) {
|
||||
ll.meta.hr = parseInt(_[0].textContent);
|
||||
this._info.hr._points.push([this._info.length, ll.meta.hr]);
|
||||
this._info.hr._total += ll.meta.hr;
|
||||
}
|
||||
|
||||
_ = el[i].getElementsByTagNameNS('*', 'cad');
|
||||
if (_.length > 0) {
|
||||
ll.meta.cad = parseInt(_[0].textContent);
|
||||
this._info.cad._points.push([this._info.length, ll.meta.cad]);
|
||||
this._info.cad._total += ll.meta.cad;
|
||||
}
|
||||
|
||||
_ = el[i].getElementsByTagNameNS('*', 'atemp');
|
||||
if (_.length > 0) {
|
||||
ll.meta.atemp = parseInt(_[0].textContent);
|
||||
this._info.atemp._points.push([this._info.length, ll.meta.atemp]);
|
||||
this._info.atemp._total += ll.meta.atemp;
|
||||
}
|
||||
|
||||
if (ll.meta.ele > this._info.elevation.max) {
|
||||
this._info.elevation.max = ll.meta.ele;
|
||||
}
|
||||
|
||||
if (ll.meta.ele < this._info.elevation.min) {
|
||||
this._info.elevation.min = ll.meta.ele;
|
||||
}
|
||||
|
||||
this._info.elevation._points.push([this._info.length, ll.meta.ele]);
|
||||
this._info.duration.end = ll.meta.time;
|
||||
|
||||
if (last != null) {
|
||||
this._info.length += this._dist3d(last, ll);
|
||||
|
||||
var t = ll.meta.ele - last.meta.ele;
|
||||
if (t > 0) {
|
||||
this._info.elevation.gain += t;
|
||||
} else {
|
||||
this._info.elevation.loss += Math.abs(t);
|
||||
}
|
||||
|
||||
t = Math.abs(ll.meta.time - last.meta.time);
|
||||
this._info.duration.total += t;
|
||||
if (t < options.max_point_interval) {
|
||||
this._info.duration.moving += t;
|
||||
}
|
||||
} else if (this._info.duration.start == null) {
|
||||
this._info.duration.start = ll.meta.time;
|
||||
}
|
||||
|
||||
last = ll;
|
||||
coords.push(ll);
|
||||
}
|
||||
|
||||
// add track
|
||||
var l = new L.Polyline(coords, this._extract_styling(line, polyline_options, options.polyline_options));
|
||||
this.fire('addline', { line: l, element: line });
|
||||
layers.push(l);
|
||||
|
||||
if (options.marker_options.startIcon || options.marker_options.startIconUrl) {
|
||||
// add start pin
|
||||
var marker = new L.Marker(coords[0], {
|
||||
clickable: options.marker_options.clickable,
|
||||
icon: options.marker_options.startIcon || new L.GPXTrackIcon({iconUrl: options.marker_options.startIconUrl})
|
||||
});
|
||||
this.fire('addpoint', { point: marker, point_type: 'start', element: el[0] });
|
||||
layers.push(marker);
|
||||
}
|
||||
|
||||
if (options.marker_options.endIcon || options.marker_options.endIconUrl) {
|
||||
// add end pin
|
||||
var marker = new L.Marker(coords[coords.length-1], {
|
||||
clickable: options.marker_options.clickable,
|
||||
icon: options.marker_options.endIcon || new L.GPXTrackIcon({iconUrl: options.marker_options.endIconUrl})
|
||||
});
|
||||
this.fire('addpoint', { point: marker, point_type: 'end', element: el[el.length-1] });
|
||||
layers.push(marker);
|
||||
}
|
||||
|
||||
// add named markers
|
||||
for (var i = 0; i < markers.length; i++) {
|
||||
var marker = new L.Marker(markers[i].coords, {
|
||||
clickable: options.marker_options.clickable,
|
||||
title: markers[i].label,
|
||||
icon: markers[i].icon
|
||||
});
|
||||
this.fire('addpoint', { point: marker, point_type: 'label', element: markers[i].element });
|
||||
layers.push(marker);
|
||||
}
|
||||
|
||||
return layers;
|
||||
},
|
||||
|
||||
_extract_styling: function(el, base, overrides) {
|
||||
var style = this._merge_objs(_DEFAULT_POLYLINE_OPTS, base);
|
||||
var e = el.getElementsByTagNameNS(_GPX_STYLE_NS, 'line');
|
||||
if (e.length > 0) {
|
||||
var _ = e[0].getElementsByTagName('color');
|
||||
if (_.length > 0) style.color = '#' + _[0].textContent;
|
||||
var _ = e[0].getElementsByTagName('opacity');
|
||||
if (_.length > 0) style.opacity = _[0].textContent;
|
||||
var _ = e[0].getElementsByTagName('weight');
|
||||
if (_.length > 0) style.weight = _[0].textContent;
|
||||
var _ = e[0].getElementsByTagName('linecap');
|
||||
if (_.length > 0) style.lineCap = _[0].textContent;
|
||||
}
|
||||
return this._merge_objs(style, overrides)
|
||||
},
|
||||
|
||||
_dist2d: function(a, b) {
|
||||
var R = 6371000;
|
||||
var dLat = this._deg2rad(b.lat - a.lat);
|
||||
var dLon = this._deg2rad(b.lng - a.lng);
|
||||
var r = Math.sin(dLat/2) *
|
||||
Math.sin(dLat/2) +
|
||||
Math.cos(this._deg2rad(a.lat)) *
|
||||
Math.cos(this._deg2rad(b.lat)) *
|
||||
Math.sin(dLon/2) *
|
||||
Math.sin(dLon/2);
|
||||
var c = 2 * Math.atan2(Math.sqrt(r), Math.sqrt(1-r));
|
||||
var d = R * c;
|
||||
return d;
|
||||
},
|
||||
|
||||
_dist3d: function(a, b) {
|
||||
var planar = this._dist2d(a, b);
|
||||
var height = Math.abs(b.meta.ele - a.meta.ele);
|
||||
return Math.sqrt(Math.pow(planar, 2) + Math.pow(height, 2));
|
||||
},
|
||||
|
||||
_deg2rad: function(deg) {
|
||||
return deg * Math.PI / 180;
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof module === 'object' && typeof module.exports === 'object') {
|
||||
module.exports = L;
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define(L);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "leaflet-gpx",
|
||||
"version": "1.5.0",
|
||||
"description": "A Leaflet plugin for showing a GPX track on a map",
|
||||
"main": "gpx.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mpetazzoni/leaflet-gpx.git"
|
||||
},
|
||||
"keywords": [
|
||||
"leaflet",
|
||||
"gpx",
|
||||
"leaflet-gpx",
|
||||
"map",
|
||||
"gps"
|
||||
],
|
||||
"author": "Maxime Petazzoni <maxime.petazzoni@bulix.org> (https://www.bulix.org)",
|
||||
"license": "BSD-2-Clause",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mpetazzoni/leaflet-gpx/issues"
|
||||
},
|
||||
"homepage": "https://github.com/mpetazzoni/leaflet-gpx#readme"
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 960 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 971 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 660 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Reference in new issue
Block a user