diff --git a/README.md b/README.md
index bcedbf67..3fcd7a56 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# leaflet-elevation.js
A Leaflet plugin that allows to add elevation profiles using d3js
-_For a working example see [demo](https://raruto.github.io/examples/leaflet-elevation/leaflet-elevation.html) and [demo](https://raruto.github.io/examples/leaflet-elevation/leaflet-elevation_hidden-map.html)_
+_For a working example see demos: [#1](https://raruto.github.io/examples/leaflet-elevation/leaflet-elevation.html), [#2](https://raruto.github.io/examples/leaflet-elevation/leaflet-elevation_hidden-map.html) and [#3](https://raruto.github.io/examples/leaflet-elevation/leaflet-elevation_geojson-data.html)_
---
@@ -18,8 +18,8 @@ _For a working example see [demo](https://raruto.github.io/examples/leaflet-elev
-
-
+
+
...
```
diff --git a/leaflet-elevation.js b/leaflet-elevation.js
index 38fb37bf..e3b99aca 100644
--- a/leaflet-elevation.js
+++ b/leaflet-elevation.js
@@ -1,947 +1,1006 @@
L.Control.Elevation = L.Control.extend({
- options: {
- position: "topright",
- theme: "lime-theme",
- width: 600,
- height: 175,
- margins: {
- top: 10,
- right: 20,
- bottom: 30,
- left: 50
- },
- useHeightIndicator: true,
- autoHideHeightIndicator: false,
- interpolation: d3.curveLinear,
- hoverNumber: {
- decimalsX: 2,
- decimalsY: 0,
- formatter: undefined
- },
- xTicks: undefined,
- yTicks: undefined,
- collapsed: false,
- yAxisMin: undefined,
- yAxisMax: undefined,
- forceAxisBounds: false,
- controlButton: {
- iconCssClass: "elevation-toggle-icon",
- title: "Elevation"
- },
- imperial: false,
- elevationDiv: "#elevation-div",
- detachedView: false,
- responsiveView: false,
- gpxOptions: {
- async: true,
- marker_options: {
- startIconUrl: null,
- endIconUrl: null,
- shadowUrl: null,
- },
- polyline_options: {
- color: '#FF005E',
- opacity: 0.75,
- weight: 5,
- lineCap: 'round'
- },
- },
- },
- __mileFactor: 0.621371,
- __footFactor: 3.28084,
-
- onRemove: function(map) {
- this._container = null;
- },
-
- onAdd: function(map) {
- this._map = map;
-
- var opts = this.options;
- var margin = opts.margins;
- opts.xTicks = opts.xTicks || Math.round(this._width() / 75);
- opts.yTicks = opts.yTicks || Math.round(this._height() / 30);
- opts.hoverNumber.formatter = opts.hoverNumber.formatter || this._formatter;
-
- if (opts.detachedView && opts.responsiveView) {
- var offsetWi = document.querySelector(opts.elevationDiv).offsetWidth;
- var offsetHe = document.querySelector(opts.elevationDiv).offsetHeight;
- opts.width = offsetWi > 0 ? offsetWi : opts.width;
- opts.height = (offsetHe - 20) > 0 ? offsetHe - 20 : opts.height - 20;
- }
-
- var x = this._x = d3.scaleLinear()
- .range([0, this._width()]);
-
- var y = this._y = d3.scaleLinear()
- .range([this._height(), 0]);
-
- var area = this._area = d3.area()
- .curve(opts.interpolation)
- .x(function(d) {
- var xDiagCoord = x(d.dist);
- d.xDiagCoord = xDiagCoord;
- return xDiagCoord;
- })
- .y0(this._height())
- .y1(function(d) {
- return y(d.altitude);
- });
-
- var container = this._container = L.DomUtil.create("div", "elevation");
- L.DomUtil.addClass(container, opts.theme); //append theme to control
-
- this._initToggle();
-
- var cont = d3.select(container);
- cont.attr("width", opts.width);
- var svg = cont.append("svg");
- svg.attr("width", opts.width)
- .attr("class", "background")
- .attr("height", opts.height)
- .style("overflow", "visible")
- .append("g")
- .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
-
- var line = d3.line();
- line = line
- .x(function(d) {
- return d3.mouse(svg.select("g"))[0];
- })
- .y(function(d) {
- return this._height();
- });
-
- var g = d3.select(this._container).select("svg").select("g");
-
- this._areapath = g.append("path")
- .attr("class", "area")
- .style("paint-order", "stroke fill");
-
- var background = this._background = g.append("rect")
- .attr("width", this._width())
- .attr("height", this._height())
- .style("fill", "none")
- .style("stroke", "none")
- .style("pointer-events", "all");
-
- if (L.Browser.touch) {
- background
- .on("touchmove.drag", this._dragHandler.bind(this))
- .on("touchstart.drag", this._dragStartHandler.bind(this))
- .on("touchstart.focus", this._mousemoveHandler.bind(this));
- L.DomEvent.on(this._container, 'touchend', this._dragEndHandler, this);
- }
-
- background
- .on("mousemove.drag", this._dragHandler.bind(this))
- .on("mousedown.drag", this._dragStartHandler.bind(this))
- .on("mousemove.focus", this._mousemoveHandler.bind(this))
- .on("mouseout.focus", this._mouseoutHandler.bind(this));
- L.DomEvent.on(this._container, 'mouseup', this._dragEndHandler, this);
-
- this._xaxisgraphicnode = g.append("g");
- this._yaxisgraphicnode = g.append("g");
- this._appendXaxis(this._xaxisgraphicnode);
- this._appendYaxis(this._yaxisgraphicnode);
-
- var focusG = this._focusG = g.append("g")
- .attr("class", "mouse-focus-group");
-
- this._mousefocus = focusG.append('svg:line')
- .attr('class', 'mouse-focus-line')
- .attr('x2', '0')
- .attr('y2', '0')
- .attr('x1', '0')
- .attr('y1', '0');
-
- this._focuslabelrect = focusG.append("rect")
- .attr('class', 'mouse-focus-label')
- .attr("x", 0)
- .attr("y", 0)
- .attr("width", 0)
- .attr("height", 0)
- .attr("rx", 3)
- .attr("ry", 3)
- .style("fill", "#000")
- .style("fill-opacity", 0.75)
- .style("pointer-events", "none");
-
- this._focuslabeltext = focusG.append("svg:text")
- .style("pointer-events", "none")
- .attr("class", "mouse-focus-label-text")
- .style("fill", "#fff");
- this._focuslabelX = this._focuslabeltext.append("svg:tspan")
- .attr("class", "mouse-focus-label-y")
- .attr("dy", "-1em");
- this._focuslabelY = this._focuslabeltext.append("svg:tspan")
- .attr("class", "mouse-focus-label-y")
- .attr("dy", "2em");
-
- if (this._data) {
- this._applyData();
- }
-
- this._map.on('zoom viewreset zoomanim', this._forceHidePositionMarker, this);
- this._map.on('resize', this._resetView, this);
-
- return container;
- },
-
- _dragHandler: function() {
- //we don´t want map events to occur here
- d3.event.preventDefault();
- d3.event.stopPropagation();
-
- this._gotDragged = true;
- this._drawDragRectangle();
- },
-
- _dragStartHandler: function() {
- d3.event.preventDefault();
- d3.event.stopPropagation();
-
- this._gotDragged = false;
- this._dragStartCoords = d3.mouse(this._background.node());
- },
-
- /*
- * Draws the currently dragged rectangle over the chart.
- */
- _drawDragRectangle: function() {
- if (!this._dragStartCoords) {
- return;
- }
-
- var dragEndCoords = this._dragCurrentCoords = d3.mouse(this._background.node());
-
- var x1 = Math.min(this._dragStartCoords[0], dragEndCoords[0]),
- x2 = Math.max(this._dragStartCoords[0], dragEndCoords[0]);
-
- if (!this._dragRectangle && !this._dragRectangleG) {
- var g = d3.select(this._container).select("svg").select("g");
-
- this._dragRectangleG = g.insert("g", ".mouse-focus-group");
-
- this._dragRectangle = this._dragRectangleG.append("rect")
- .attr("width", x2 - x1)
- .attr("height", this._height())
- .attr("x", x1)
- .attr('class', 'mouse-drag')
- .style("pointer-events", "none");
- } else {
- this._dragRectangle.attr("width", x2 - x1)
- .attr("x", x1);
- }
- },
-
- /*
- * Handles end of dragg operations. Zooms the map to the selected items extent.
- */
- _dragEndHandler: function() {
- if (!this._dragStartCoords || !this._gotDragged) {
- this._dragStartCoords = null;
- this._gotDragged = false;
- //this._resetDrag();
- return;
- }
-
- this._hidePositionMarker();
-
- var item1 = this._findItemForX(this._dragStartCoords[0]),
- item2 = this._findItemForX(this._dragCurrentCoords[0]);
-
- this._fitSection(item1, item2);
-
- this._dragStartCoords = null;
- this._gotDragged = false;
-
- this._map.fireEvent("elechart_dragged", {
- data: {
- dragstart: this._data[item1],
- dragend: this._data[item2]
- }
- }, true);
- },
-
- /*
- * Removes the drag rectangle and zoms back to the total extent of the data.
- */
- _resetDrag: function() {
- if (this._dragRectangleG) {
- this._dragRectangleG.remove();
- this._dragRectangleG = null;
- this._dragRectangle = null;
- this._hidePositionMarker();
- this._map.fitBounds(this._fullExtent);
- }
- },
-
- /*
- * Finds a data entry for a given x-coordinate of the diagram
- */
- _findItemForX: function(x) {
- var bisect = d3.bisector(function(d) {
- return d.dist;
- }).left;
- var xinvert = this._x.invert(x);
- return bisect(this._data, xinvert);
- },
-
- /*
- * Finds an item with the smallest delta in distance to the given latlng coords
- */
- _findItemForLatLng: function(latlng) {
- var result = null,
- d = Infinity;
- this._data.forEach(function(item) {
- var dist = latlng.distanceTo(item.latlng);
- if (dist < d) {
- d = dist;
- result = item;
- }
- });
- return result;
- },
-
- /** Make the map fit the route section between given indexes. */
- _fitSection: function(index1, index2) {
- var start = Math.min(index1, index2),
- end = Math.max(index1, index2);
-
- var ext = this._calculateFullExtent(this._data.slice(start, end));
-
- this._map.fitBounds(ext);
- },
-
- _initToggle: function() {
- /* inspired by L.Control.Layers */
- var container = this._container;
-
- //Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
- container.setAttribute('aria-haspopup', true);
-
- L.DomEvent
- .disableClickPropagation(container);
- //.disableScrollPropagation(container);
-
- if (L.Browser.touch) {
- L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
- }
-
- if (this.options.collapsed) {
- this._collapse();
-
- if (!L.Browser.android) {
- L.DomEvent
- .on(container, 'mouseover', this._expand, this)
- .on(container, 'mouseout', this._collapse, this);
- }
- var link = this._button = L.DomUtil.create('a', "elevation-toggle " + this.options.controlButton.iconCssClass, container);
- link.href = '#';
- link.title = this.options.controlButton.title;
-
- if (L.Browser.touch) {
- L.DomEvent
- .on(link, 'click', L.DomEvent.stop)
- .on(link, 'click', this._expand, this);
- }
-
- L.DomEvent.on(link, 'focus', this._expand, this);
-
- this._map.on('click', this._collapse, this);
- // TODO keyboard accessibility
- }
- },
-
- _expand: function() {
- this._container.className = this._container.className.replace(' elevation-collapsed', '');
- },
-
- _collapse: function() {
- L.DomUtil.addClass(this._container, 'elevation-collapsed');
- },
-
- _width: function() {
- var opts = this.options;
- return opts.width - opts.margins.left - opts.margins.right;
- },
-
- _height: function() {
- var opts = this.options;
- return opts.height - opts.margins.top - opts.margins.bottom;
- },
-
- /*
- * Fromatting funciton using the given decimals and seperator
- */
- _formatter: function(num, dec, sep) {
- var res;
- if (dec === 0) {
- res = Math.round(num) + "";
- } else {
- res = L.Util.formatNum(num, dec) + "";
- }
- var numbers = res.split(".");
- if (numbers[1]) {
- var d = dec - numbers[1].length;
- for (; d > 0; d--) {
- numbers[1] += "0";
- }
- res = numbers.join(sep || ".");
- }
- return res;
- },
-
- _appendYaxis: function(y) {
- var opts = this.options;
-
- if (opts.imperial) {
- y
- .attr("class", "y axis")
- .call(
- d3
- .axisLeft()
- .scale(this._y)
- .ticks(this.options.yTicks)
- )
- .append("text")
- .attr("x", -37)
- .attr("y", 3)
- .style("text-anchor", "end")
- .text("ft");
- } else {
- y
- .attr("class", "y axis")
- .call(
- d3
- .axisLeft()
- .scale(this._y)
- .ticks(this.options.yTicks)
- )
- .append("text")
- .attr("x", -30)
- .attr("y", 3)
- .style("text-anchor", "end")
- .style("fill", "#000")
- .style("font-weight", "700")
- .text("m");
- }
- },
-
- _appendXaxis: function(x) {
- var opts = this.options;
-
- if (opts.imperial) {
- x
- .attr("class", "x axis")
- .attr("transform", "translate(0," + this._height() + ")")
- .call(
- d3
- .axisBottom()
- .scale(this._x)
- .ticks(this.options.xTicks)
- )
- .append("text")
- .attr("x", this._width() + 10)
- .attr("y", 15)
- .style("text-anchor", "end")
- .text("mi");
- } else {
- x
- .attr("class", "x axis")
- .attr("transform", "translate(0," + this._height() + ")")
- .call(
- d3
- .axisBottom()
- .scale(this._x)
- .ticks(this.options.xTicks)
- )
- .append("text")
- .attr("x", this._width() + 6)
- .attr("y", 30)
- .style("text-anchor", "end")
- .style("fill", "#000")
- .style("font-weight", "700")
- .text("km");
- }
- },
-
- _updateAxis: function() {
- this._xaxisgraphicnode.selectAll("g").remove();
- this._xaxisgraphicnode.selectAll("path").remove();
- this._xaxisgraphicnode.selectAll("text").remove();
- this._yaxisgraphicnode.selectAll("g").remove();
- this._yaxisgraphicnode.selectAll("path").remove();
- this._yaxisgraphicnode.selectAll("text").remove();
- this._appendXaxis(this._xaxisgraphicnode);
- this._appendYaxis(this._yaxisgraphicnode);
- },
-
- _mouseoutHandler: function() {
- //this._hidePositionMarker();
- },
-
- _resetView: function() {
- this._resetDrag();
- this._forceHidePositionMarker();
- },
-
- _forceHidePositionMarker: function() {
- this._hidePositionMarker(true);
- },
-
- /*
- * Hides the position-/heigth indication marker drawn onto the map
- */
- _hidePositionMarker: function(force) {
- if (!this.options.autoHideHeightIndicator && !force) {
- return;
- }
-
- if (this._marker) {
- this._map.removeLayer(this._marker);
- this._marker = null;
- }
- if (this._mouseHeightFocus) {
- this._mouseHeightFocus.style("visibility", "hidden");
- this._mouseHeightFocusLabel.style("visibility", "hidden");
- }
- if (this._pointG) {
- this._pointG.style("visibility", "hidden");
- }
- this._focusG.style("visibility", "hidden");
- },
-
- /*
- * Handles the moueseover the chart and displays distance and altitude level
- */
- _mousemoveHandler: function(d, i, ctx) {
- if (!this._data || this._data.length === 0) {
- return;
- }
- var coords = d3.mouse(this._background.node());
- var item = this._data[this._findItemForX(coords[0])];
-
- this._forceHidePositionMarker();
- this._showDiagramIndicator(item, coords[0]);
- this._showPositionMarker(item);
-
- this._map.fireEvent("elechart_change", {
- data: item
- }, true);
-
- },
-
- _showPositionMarker: function(item) {
- var opts = this.options,
- alt = item.altitude,
- dist = item.dist,
- ll = item.latlng,
- numY = opts.hoverNumber.formatter(alt, opts.hoverNumber.decimalsY),
- numX = opts.hoverNumber.formatter(dist, opts.hoverNumber.decimalsX);
-
- var layerpoint = this._map.latLngToLayerPoint(ll);
-
- //if we use a height indicator we create one with SVG
- //otherwise we show a marker
- if (opts.useHeightIndicator) {
- if (!this._mouseHeightFocus) {
- var heightG = d3.select(this._map.getContainer()).select(".leaflet-overlay-pane svg")
- .append("g");
- this._mouseHeightFocus = heightG.append('svg:line')
- .attr("class", opts.theme + " height-focus line")
- .attr("x2", 0)
- .attr("y2", 0)
- .attr("x1", 0)
- .attr("y1", 0);
-
- var pointG = this._pointG = heightG.append("g");
- pointG.append("svg:circle")
- .attr("r", 6)
- .attr("cx", 0)
- .attr("cy", 0)
- .attr("class", opts.theme + " height-focus circle-lower");
-
- this._mouseHeightFocusLabel = heightG.append("svg:text")
- .attr("class", opts.theme + " height-focus-label")
- .style("pointer-events", "none");
-
- }
-
- var normalizedAlt = this._height() / this._maxElevation * alt;
- var normalizedY = layerpoint.y - normalizedAlt;
- this._mouseHeightFocus.attr("x1", layerpoint.x)
- .attr("x2", layerpoint.x)
- .attr("y1", layerpoint.y)
- .attr("y2", normalizedY)
- .style("visibility", "visible");
-
- this._pointG.attr("transform", "translate(" + layerpoint.x + "," + layerpoint.y + ")")
- .style("visibility", "visible");
-
- if (opts.imperial) {
- this._mouseHeightFocusLabel.attr("x", layerpoint.x)
- .attr("y", normalizedY)
- .text(numY + " ft")
- .style("visibility", "visible");
- } else {
- this._mouseHeightFocusLabel.attr("x", layerpoint.x)
- .attr("y", normalizedY)
- .text(numY + " m")
- .style("visibility", "visible");
- }
- } else {
- if (!this._marker) {
- this._marker = new L.Marker(ll).addTo(this._map);
- } else {
- this._marker.setLatLng(ll);
- }
- }
- },
-
- /*
- * Parsing of GeoJSON data lines and their elevation in z-coordinate
- */
- _addGeoJSONData: function(coords) {
- var opts = this.options;
- if (coords) {
- var data = this._data || [];
- var dist = this._dist || 0;
- var ele = this._maxElevation || 0;
- for (var i = 0; i < coords.length; i++) {
- var s = new L.LatLng(coords[i][1], coords[i][0]);
- var e = new L.LatLng(coords[i ? i - 1 : 0][1], coords[i ? i - 1 : 0][0]);
- var newdist = opts.imperial ? s.distanceTo(e) * this.__mileFactor : s.distanceTo(e);
- dist = dist + Math.round(newdist / 1000 * 100000) / 100000;
-
- // skip point if it has not elevation
- if (typeof coords[i][2] !== "undefined") {
- ele = ele < coords[i][2] ? coords[i][2] : ele;
- data.push({
- dist: dist,
- altitude: opts.imperial ? coords[i][2] * this.__footFactor : coords[i][2],
- x: coords[i][0],
- y: coords[i][1],
- latlng: s
- });
- }
- }
- this._dist = dist;
- this._data = data;
- ele = opts.imperial ? ele * this.__footFactor : ele;
- this._maxElevation = ele;
- }
- },
-
- /*
- * Parsing function for GPX data as used by https://github.com/mpetazzoni/leaflet-gpx
- */
- _addGPXdata: function(coords) {
- var opts = this.options;
- if (coords) {
- var data = this._data || [];
- var dist = this._dist || 0;
- var ele = this._maxElevation || 0;
- for (var i = 0; i < coords.length; i++) {
- var s = coords[i];
- var e = coords[i ? i - 1 : 0];
- var newdist = opts.imperial ? s.distanceTo(e) * this.__mileFactor : s.distanceTo(e);
- dist = dist + Math.round(newdist / 1000 * 100000) / 100000;
- // skip point if it has not elevation
- if (typeof s.meta.ele !== "undefined") {
- ele = ele < s.meta.ele ? s.meta.ele : ele;
- data.push({
- dist: dist,
- altitude: opts.imperial ? s.meta.ele * this.__footFactor : s.meta.ele,
- x: s.lng,
- y: s.lat,
- latlng: s
- });
- }
- }
- this._dist = dist;
- this._data = data;
- ele = opts.imperial ? ele * this.__footFactor : ele;
- this._maxElevation = ele;
- }
- },
-
- _addData: function(d) {
- var geom = d && d.geometry && d.geometry;
- var i;
-
- if (geom) {
- switch (geom.type) {
- case 'LineString':
- this._addGeoJSONData(geom.coordinates);
- break;
-
- case 'MultiLineString':
- for (i = 0; i < geom.coordinates.length; i++) {
- this._addGeoJSONData(geom.coordinates[i]);
- }
- break;
-
- default:
- throw new Error('Invalid GeoJSON object.');
- }
- }
-
- var feat = d && d.type === "FeatureCollection";
- if (feat) {
- for (i = 0; i < d.features.length; i++) {
- this._addData(d.features[i]);
- }
- }
-
- if (d && d._latlngs) {
- this._addGPXdata(d._latlngs);
- }
- },
-
- /*
- * Calculates the full extent of the data array
- */
- _calculateFullExtent: function(data) {
- if (!data || data.length < 1) {
- throw new Error("no data in parameters");
- }
-
- var ext = new L.latLngBounds(data[0].latlng, data[0].latlng);
-
- data.forEach(function(item) {
- ext.extend(item.latlng);
- });
-
- return ext;
- },
-
- /*
- * Add data to the diagram either from GPX or GeoJSON and
- * update the axis domain and data
- */
- addData: function(d, layer) {
- this._addData(d);
- if (this._container) {
- this._applyData();
- }
- if (layer === null && d.on) {
- layer = d;
- }
- if (layer) {
- layer
- .on("mousemove", this._handleLayerMouseOver.bind(this))
- .on("mouseout", this._mouseoutHandler.bind(this));
- }
- },
-
- addGPXFile: function(url) {
- if (!url) {
- throw "Invalid gpx url";
- }
-
- var themeName = this.options.theme;
- // TODO: find a better workaround for: this.gpx.setStyle({className: themeName});
- var themeColor = themeName.substring(0, themeName.indexOf("-theme"));
- var hexColor;
- switch (themeColor) {
- case 'lime':
- hexColor = '#566B13';
- break;
- case 'steelblue':
- hexColor = '#4682B4';
- break;
- case 'purple':
- hexColor = '#732C7B';
- break;
- case 'magenta':
- hexColor = '#FF005E';
- break;
- default:
- hexColor = this.options.gpxOptions.polyline_options.color;
- break;
- }
- this.options.gpxOptions.polyline_options.color = hexColor;
-
- this.gpx = new L.GPX(url, this.options.gpxOptions);
-
- this.gpx.on('loaded', function(e) {
- this._map.fitBounds(e.target.getBounds());
- });
- this.gpx.once("addline", function(e) {
- this.addData(e.line, this.gpx);
- }, this);
-
- this.gpx.addTo(this._map);
-
- this._map.on('resize', function(e) {
- var mapDiv = this._map.getContainer();
- var eleDiv = document.querySelector(this.options.elevationDiv);
-
- var newWidth = eleDiv.offsetWidth; // - 20;
-
- if(newWidth <= 0) return;
-
- this.options.width = newWidth;
-
- eleDiv.innerHTML = "";
-
- var container = this.onAdd(this._map);
- container.classList.add("leaflet-control");
-
- if (this.options.detachedView) {
- eleDiv.appendChild(container);
- } else {
- this.addTo(this._map);
- }
- }, this);
-
- },
-
- /*
- * Handles mouseover events of the data layers on the map.
- */
- _handleLayerMouseOver: function(evt) {
- if (!this._data || this._data.length === 0) {
- return;
- }
- var latlng = evt.latlng;
- var item = this._findItemForLatLng(latlng);
- if (item) {
- var x = item.xDiagCoord;
-
- this._forceHidePositionMarker();
- this._showDiagramIndicator(item, x);
- this._showPositionMarker(item);
- }
- },
-
- _showDiagramIndicator: function(item, xCoordinate) {
- var opts = this.options;
- this._focusG.style("visibility", "visible");
- this._mousefocus.attr('x1', xCoordinate)
- .attr('y1', 0)
- .attr('x2', xCoordinate)
- .attr('y2', this._height())
- .classed('hidden', false);
-
- var alt = item.altitude,
- dist = item.dist,
- ll = item.latlng,
- numY = opts.hoverNumber.formatter(alt, opts.hoverNumber.decimalsY),
- numX = opts.hoverNumber.formatter(dist, opts.hoverNumber.decimalsX);
-
- this._focuslabeltext
- .attr("x", xCoordinate)
- .attr("y", this._y(item.altitude))
- .style("font-weight", "700");
-
- this._focuslabelX
- .text(numY + (opts.imperial ? " ft" : " m"))
- .attr("x", xCoordinate + 10);
- this._focuslabelY
- .text(numX + (opts.imperial ? " mi" : " km"))
- .attr("x", xCoordinate + 10);
-
- var bbox = this._focuslabeltext.node().getBBox();
- var padding = 2;
-
- this._focuslabelrect
- .attr("x", bbox.x - padding)
- .attr("y", bbox.y - padding)
- .attr("width", bbox.width + (padding * 2))
- .attr("height", bbox.height + (padding * 2));
-
- },
-
- _applyData: function() {
- var xdomain = d3.extent(this._data, function(d) {
- return d.dist;
- });
- var ydomain = d3.extent(this._data, function(d) {
- return d.altitude;
- });
- var opts = this.options;
-
- if (opts.yAxisMin !== undefined && (opts.yAxisMin < ydomain[0] || opts.forceAxisBounds)) {
- ydomain[0] = opts.yAxisMin;
- }
- if (opts.yAxisMax !== undefined && (opts.yAxisMax > ydomain[1] || opts.forceAxisBounds)) {
- ydomain[1] = opts.yAxisMax;
- }
-
- this._x.domain(xdomain);
- this._y.domain(ydomain);
- this._areapath.datum(this._data)
- .attr("d", this._area);
- this._updateAxis();
-
- this._fullExtent = this._calculateFullExtent(this._data);
- },
-
- /*
- * Reset data
- */
- _clearData: function() {
- this._data = null;
- this._dist = null;
- this._maxElevation = null;
- // if (this.gpx) {
- // this.gpx.removeFrom(this._map);
- // }
- },
-
- /*
- * Reset data and display
- */
- clear: function() {
- this._clearData();
-
- if (!this._areapath) {
- return;
- }
-
- // workaround for 'Error: Problem parsing d=""' in Webkit when empty data
- // https://groups.google.com/d/msg/d3-js/7rFxpXKXFhI/HzIO_NPeDuMJ
- //this._areapath.datum(this._data).attr("d", this._area);
- this._areapath.attr("d", "M0 0");
-
- this._x.domain([0, 1]);
- this._y.domain([0, 1]);
- this._updateAxis();
- },
-
- hide: function() {
- this._container.style.display = "none";
- },
-
- show: function() {
- this._container.style.display = "block";
- },
-
- _addToElevationDiv: function(map) {
- var container = this.onAdd(map);
- container.classList.add("leaflet-control");
- var eleDiv = document.querySelector(this.options.elevationDiv);
- eleDiv.appendChild(container);
- },
-
- loadChart: function(map) {
+ options: {
+ position: "topright",
+ theme: "lime-theme",
+ width: 600,
+ height: 175,
+ margins: {
+ top: 10,
+ right: 20,
+ bottom: 30,
+ left: 50
+ },
+ useHeightIndicator: true,
+ autoHideHeightIndicator: false,
+ interpolation: d3.curveLinear,
+ hoverNumber: {
+ decimalsX: 2,
+ decimalsY: 0,
+ formatter: undefined
+ },
+ xTicks: undefined,
+ yTicks: undefined,
+ collapsed: false,
+ yAxisMin: undefined,
+ yAxisMax: undefined,
+ forceAxisBounds: false,
+ controlButton: {
+ iconCssClass: "elevation-toggle-icon",
+ title: "Elevation"
+ },
+ imperial: false,
+ elevationDiv: "#elevation-div",
+ detachedView: false,
+ responsiveView: false,
+ gpxOptions: {
+ async: true,
+ marker_options: {
+ startIconUrl: null,
+ endIconUrl: null,
+ shadowUrl: null,
+ },
+ polyline_options: {
+ color: '#FF005E',
+ opacity: 0.75,
+ weight: 5,
+ lineCap: 'round'
+ },
+ },
+ },
+ __mileFactor: 0.621371,
+ __footFactor: 3.28084,
+
+ onRemove: function(map) {
+ this._container = null;
+ },
+
+ onAdd: function(map) {
+ this._map = map;
+
+ var opts = this.options;
+ var margin = opts.margins;
+ opts.xTicks = opts.xTicks || Math.round(this._width() / 75);
+ opts.yTicks = opts.yTicks || Math.round(this._height() / 30);
+ opts.hoverNumber.formatter = opts.hoverNumber.formatter || this._formatter;
+
+ if (opts.detachedView && opts.responsiveView) {
+ var offsetWi = document.querySelector(opts.elevationDiv).offsetWidth;
+ var offsetHe = document.querySelector(opts.elevationDiv).offsetHeight;
+ opts.width = offsetWi > 0 ? offsetWi : opts.width;
+ opts.height = (offsetHe - 20) > 0 ? offsetHe - 20 : opts.height - 20;
+ }
+
+ var x = this._x = d3.scaleLinear()
+ .range([0, this._width()]);
+
+ var y = this._y = d3.scaleLinear()
+ .range([this._height(), 0]);
+
+ var area = this._area = d3.area()
+ .curve(opts.interpolation)
+ .x(function(d) {
+ var xDiagCoord = x(d.dist);
+ d.xDiagCoord = xDiagCoord;
+ return xDiagCoord;
+ })
+ .y0(this._height())
+ .y1(function(d) {
+ return y(d.altitude);
+ });
+
+ var container = this._container = L.DomUtil.create("div", "elevation");
+ L.DomUtil.addClass(container, opts.theme); //append theme to control
+
+ this._initToggle();
+
+ var cont = d3.select(container);
+ cont.attr("width", opts.width);
+ var svg = cont.append("svg");
+ svg.attr("width", opts.width)
+ .attr("class", "background")
+ .attr("height", opts.height)
+ .style("overflow", "visible")
+ .append("g")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+ var line = d3.line();
+ line = line
+ .x(function(d) {
+ return d3.mouse(svg.select("g"))[0];
+ })
+ .y(function(d) {
+ return this._height();
+ });
+
+ var g = d3.select(this._container).select("svg").select("g");
+
+ this._areapath = g.append("path")
+ .attr("class", "area")
+ .style("paint-order", "stroke fill");
+
+ var background = this._background = g.append("rect")
+ .attr("width", this._width())
+ .attr("height", this._height())
+ .style("fill", "none")
+ .style("stroke", "none")
+ .style("pointer-events", "all");
+
+ if (L.Browser.touch) {
+ background
+ .on("touchmove.drag", this._dragHandler.bind(this))
+ .on("touchstart.drag", this._dragStartHandler.bind(this))
+ .on("touchstart.focus", this._mousemoveHandler.bind(this));
+ L.DomEvent.on(this._container, 'touchend', this._dragEndHandler, this);
+ }
+
+ background
+ .on("mousemove.drag", this._dragHandler.bind(this))
+ .on("mousedown.drag", this._dragStartHandler.bind(this))
+ .on("mousemove.focus", this._mousemoveHandler.bind(this))
+ .on("mouseout.focus", this._mouseoutHandler.bind(this));
+ L.DomEvent.on(this._container, 'mouseup', this._dragEndHandler, this);
+
+ this._xaxisgraphicnode = g.append("g");
+ this._yaxisgraphicnode = g.append("g");
+ this._appendXaxis(this._xaxisgraphicnode);
+ this._appendYaxis(this._yaxisgraphicnode);
+
+ var focusG = this._focusG = g.append("g")
+ .attr("class", "mouse-focus-group");
+
+ this._mousefocus = focusG.append('svg:line')
+ .attr('class', 'mouse-focus-line')
+ .attr('x2', '0')
+ .attr('y2', '0')
+ .attr('x1', '0')
+ .attr('y1', '0');
+
+ this._focuslabelrect = focusG.append("rect")
+ .attr('class', 'mouse-focus-label')
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", 0)
+ .attr("height", 0)
+ .attr("rx", 3)
+ .attr("ry", 3)
+ .style("fill", "#000")
+ .style("fill-opacity", 0.75)
+ .style("pointer-events", "none");
+
+ this._focuslabeltext = focusG.append("svg:text")
+ .style("pointer-events", "none")
+ .attr("class", "mouse-focus-label-text")
+ .style("fill", "#fff");
+ this._focuslabelX = this._focuslabeltext.append("svg:tspan")
+ .attr("class", "mouse-focus-label-y")
+ .attr("dy", "-1em");
+ this._focuslabelY = this._focuslabeltext.append("svg:tspan")
+ .attr("class", "mouse-focus-label-y")
+ .attr("dy", "2em");
+
+ if (this._data) {
+ this._applyData();
+ }
+
+ this._map.on('zoom viewreset zoomanim', this._forceHidePositionMarker, this);
+ this._map.on('resize', this._resetView, this);
+
+ return container;
+ },
+
+ _dragHandler: function() {
+ //we don´t want map events to occur here
+ d3.event.preventDefault();
+ d3.event.stopPropagation();
+
+ this._gotDragged = true;
+ this._drawDragRectangle();
+ },
+
+ _dragStartHandler: function() {
+ d3.event.preventDefault();
+ d3.event.stopPropagation();
+
+ this._gotDragged = false;
+ this._dragStartCoords = d3.mouse(this._background.node());
+ },
+
+ /*
+ * Draws the currently dragged rectangle over the chart.
+ */
+ _drawDragRectangle: function() {
+ if (!this._dragStartCoords) {
+ return;
+ }
+
+ var dragEndCoords = this._dragCurrentCoords = d3.mouse(this._background.node());
+
+ var x1 = Math.min(this._dragStartCoords[0], dragEndCoords[0]),
+ x2 = Math.max(this._dragStartCoords[0], dragEndCoords[0]);
+
+ if (!this._dragRectangle && !this._dragRectangleG) {
+ var g = d3.select(this._container).select("svg").select("g");
+
+ this._dragRectangleG = g.insert("g", ".mouse-focus-group");
+
+ this._dragRectangle = this._dragRectangleG.append("rect")
+ .attr("width", x2 - x1)
+ .attr("height", this._height())
+ .attr("x", x1)
+ .attr('class', 'mouse-drag')
+ .style("pointer-events", "none");
+ } else {
+ this._dragRectangle.attr("width", x2 - x1)
+ .attr("x", x1);
+ }
+ },
+
+ /*
+ * Handles end of dragg operations. Zooms the map to the selected items extent.
+ */
+ _dragEndHandler: function() {
+ if (!this._dragStartCoords || !this._gotDragged) {
+ this._dragStartCoords = null;
+ this._gotDragged = false;
+ //this._resetDrag();
+ return;
+ }
+
+ this._hidePositionMarker();
+
+ var item1 = this._findItemForX(this._dragStartCoords[0]),
+ item2 = this._findItemForX(this._dragCurrentCoords[0]);
+
+ this._fitSection(item1, item2);
+
+ this._dragStartCoords = null;
+ this._gotDragged = false;
+
+ this._map.fireEvent("elechart_dragged", {
+ data: {
+ dragstart: this._data[item1],
+ dragend: this._data[item2]
+ }
+ }, true);
+ },
+
+ /*
+ * Removes the drag rectangle and zoms back to the total extent of the data.
+ */
+ _resetDrag: function() {
+ if (this._dragRectangleG) {
+ this._dragRectangleG.remove();
+ this._dragRectangleG = null;
+ this._dragRectangle = null;
+ this._hidePositionMarker();
+ this._map.fitBounds(this._fullExtent);
+ }
+ },
+
+ /*
+ * Finds a data entry for a given x-coordinate of the diagram
+ */
+ _findItemForX: function(x) {
+ var bisect = d3.bisector(function(d) {
+ return d.dist;
+ }).left;
+ var xinvert = this._x.invert(x);
+ return bisect(this._data, xinvert);
+ },
+
+ /*
+ * Finds an item with the smallest delta in distance to the given latlng coords
+ */
+ _findItemForLatLng: function(latlng) {
+ var result = null,
+ d = Infinity;
+ this._data.forEach(function(item) {
+ var dist = latlng.distanceTo(item.latlng);
+ if (dist < d) {
+ d = dist;
+ result = item;
+ }
+ });
+ return result;
+ },
+
+ /** Make the map fit the route section between given indexes. */
+ _fitSection: function(index1, index2) {
+ var start = Math.min(index1, index2),
+ end = Math.max(index1, index2);
+
+ var ext = this._calculateFullExtent(this._data.slice(start, end));
+
+ this._map.fitBounds(ext);
+ },
+
+ _initToggle: function() {
+ /* inspired by L.Control.Layers */
+ var container = this._container;
+
+ //Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
+ container.setAttribute('aria-haspopup', true);
+
+ L.DomEvent
+ .disableClickPropagation(container);
+ //.disableScrollPropagation(container);
+
+ if (L.Browser.touch) {
+ L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
+ }
+
+ if (this.options.collapsed) {
+ this._collapse();
+
+ if (!L.Browser.android) {
+ L.DomEvent
+ .on(container, 'mouseover', this._expand, this)
+ .on(container, 'mouseout', this._collapse, this);
+ }
+ var link = this._button = L.DomUtil.create('a', "elevation-toggle " + this.options.controlButton.iconCssClass, container);
+ link.href = '#';
+ link.title = this.options.controlButton.title;
+
+ if (L.Browser.touch) {
+ L.DomEvent
+ .on(link, 'click', L.DomEvent.stop)
+ .on(link, 'click', this._expand, this);
+ }
+
+ L.DomEvent.on(link, 'focus', this._expand, this);
+
+ this._map.on('click', this._collapse, this);
+ // TODO keyboard accessibility
+ }
+ },
+
+ _expand: function() {
+ this._container.className = this._container.className.replace(' elevation-collapsed', '');
+ },
+
+ _collapse: function() {
+ L.DomUtil.addClass(this._container, 'elevation-collapsed');
+ },
+
+ _width: function() {
+ var opts = this.options;
+ return opts.width - opts.margins.left - opts.margins.right;
+ },
+
+ _height: function() {
+ var opts = this.options;
+ return opts.height - opts.margins.top - opts.margins.bottom;
+ },
+
+ /*
+ * Fromatting funciton using the given decimals and seperator
+ */
+ _formatter: function(num, dec, sep) {
+ var res;
+ if (dec === 0) {
+ res = Math.round(num) + "";
+ } else {
+ res = L.Util.formatNum(num, dec) + "";
+ }
+ var numbers = res.split(".");
+ if (numbers[1]) {
+ var d = dec - numbers[1].length;
+ for (; d > 0; d--) {
+ numbers[1] += "0";
+ }
+ res = numbers.join(sep || ".");
+ }
+ return res;
+ },
+
+ _appendYaxis: function(y) {
+ var opts = this.options;
+
+ if (opts.imperial) {
+ y
+ .attr("class", "y axis")
+ .call(
+ d3
+ .axisLeft()
+ .scale(this._y)
+ .ticks(this.options.yTicks)
+ )
+ .append("text")
+ .attr("x", -37)
+ .attr("y", 3)
+ .style("text-anchor", "end")
+ .text("ft");
+ } else {
+ y
+ .attr("class", "y axis")
+ .call(
+ d3
+ .axisLeft()
+ .scale(this._y)
+ .ticks(this.options.yTicks)
+ )
+ .append("text")
+ .attr("x", -30)
+ .attr("y", 3)
+ .style("text-anchor", "end")
+ .style("fill", "#000")
+ .style("font-weight", "700")
+ .text("m");
+ }
+ },
+
+ _appendXaxis: function(x) {
+ var opts = this.options;
+
+ if (opts.imperial) {
+ x
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + this._height() + ")")
+ .call(
+ d3
+ .axisBottom()
+ .scale(this._x)
+ .ticks(this.options.xTicks)
+ )
+ .append("text")
+ .attr("x", this._width() + 10)
+ .attr("y", 15)
+ .style("text-anchor", "end")
+ .text("mi");
+ } else {
+ x
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + this._height() + ")")
+ .call(
+ d3
+ .axisBottom()
+ .scale(this._x)
+ .ticks(this.options.xTicks)
+ )
+ .append("text")
+ .attr("x", this._width() + 6)
+ .attr("y", 30)
+ .style("text-anchor", "end")
+ .style("fill", "#000")
+ .style("font-weight", "700")
+ .text("km");
+ }
+ },
+
+ _updateAxis: function() {
+ this._xaxisgraphicnode.selectAll("g").remove();
+ this._xaxisgraphicnode.selectAll("path").remove();
+ this._xaxisgraphicnode.selectAll("text").remove();
+ this._yaxisgraphicnode.selectAll("g").remove();
+ this._yaxisgraphicnode.selectAll("path").remove();
+ this._yaxisgraphicnode.selectAll("text").remove();
+ this._appendXaxis(this._xaxisgraphicnode);
+ this._appendYaxis(this._yaxisgraphicnode);
+ },
+
+ _mouseoutHandler: function() {
+ //this._hidePositionMarker();
+ },
+
+ _resetView: function() {
+ this._resetDrag();
+ this._forceHidePositionMarker();
+ },
+
+ _forceHidePositionMarker: function() {
+ this._hidePositionMarker(true);
+ },
+
+ /*
+ * Hides the position-/heigth indication marker drawn onto the map
+ */
+ _hidePositionMarker: function(force) {
+ if (!this.options.autoHideHeightIndicator && !force) {
+ return;
+ }
+
+ if (this._marker) {
+ this._map.removeLayer(this._marker);
+ this._marker = null;
+ }
+ if (this._mouseHeightFocus) {
+ this._mouseHeightFocus.style("visibility", "hidden");
+ this._mouseHeightFocusLabel.style("visibility", "hidden");
+ }
+ if (this._pointG) {
+ this._pointG.style("visibility", "hidden");
+ }
+ this._focusG.style("visibility", "hidden");
+ },
+
+ /*
+ * Handles the moueseover the chart and displays distance and altitude level
+ */
+ _mousemoveHandler: function(d, i, ctx) {
+ if (!this._data || this._data.length === 0) {
+ return;
+ }
+ var coords = d3.mouse(this._background.node());
+ var item = this._data[this._findItemForX(coords[0])];
+
+ this._forceHidePositionMarker();
+ this._showDiagramIndicator(item, coords[0]);
+ this._showPositionMarker(item);
+
+ this._map.fireEvent("elechart_change", {
+ data: item
+ }, true);
+
+ },
+
+ _showPositionMarker: function(item) {
+ var opts = this.options,
+ alt = item.altitude,
+ dist = item.dist,
+ ll = item.latlng,
+ numY = opts.hoverNumber.formatter(alt, opts.hoverNumber.decimalsY),
+ numX = opts.hoverNumber.formatter(dist, opts.hoverNumber.decimalsX);
+
+ var layerpoint = this._map.latLngToLayerPoint(ll);
+
+ //if we use a height indicator we create one with SVG
+ //otherwise we show a marker
+ if (opts.useHeightIndicator) {
+ if (!this._mouseHeightFocus) {
+ var heightG = d3.select(this._map.getContainer()).select(".leaflet-overlay-pane svg")
+ .append("g");
+ this._mouseHeightFocus = heightG.append('svg:line')
+ .attr("class", opts.theme + " height-focus line")
+ .attr("x2", 0)
+ .attr("y2", 0)
+ .attr("x1", 0)
+ .attr("y1", 0);
+
+ var pointG = this._pointG = heightG.append("g");
+ pointG.append("svg:circle")
+ .attr("r", 6)
+ .attr("cx", 0)
+ .attr("cy", 0)
+ .attr("class", opts.theme + " height-focus circle-lower");
+
+ this._mouseHeightFocusLabel = heightG.append("svg:text")
+ .attr("class", opts.theme + " height-focus-label")
+ .style("pointer-events", "none");
+
+ }
+
+ var normalizedAlt = this._height() / this._maxElevation * alt;
+ var normalizedY = layerpoint.y - normalizedAlt;
+ this._mouseHeightFocus.attr("x1", layerpoint.x)
+ .attr("x2", layerpoint.x)
+ .attr("y1", layerpoint.y)
+ .attr("y2", normalizedY)
+ .style("visibility", "visible");
+
+ this._pointG.attr("transform", "translate(" + layerpoint.x + "," + layerpoint.y + ")")
+ .style("visibility", "visible");
+
+ if (opts.imperial) {
+ this._mouseHeightFocusLabel.attr("x", layerpoint.x)
+ .attr("y", normalizedY)
+ .text(numY + " ft")
+ .style("visibility", "visible");
+ } else {
+ this._mouseHeightFocusLabel.attr("x", layerpoint.x)
+ .attr("y", normalizedY)
+ .text(numY + " m")
+ .style("visibility", "visible");
+ }
+ } else {
+ if (!this._marker) {
+ this._marker = new L.Marker(ll).addTo(this._map);
+ } else {
+ this._marker.setLatLng(ll);
+ }
+ }
+ },
+
+ /*
+ * Parsing of GeoJSON data lines and their elevation in z-coordinate
+ */
+ _addGeoJSONData: function(coords) {
+ var opts = this.options;
+ if (coords) {
+ var data = this._data || [];
+ var dist = this._dist || 0;
+ var ele = this._maxElevation || 0;
+ for (var i = 0; i < coords.length; i++) {
+ var s = new L.LatLng(coords[i][1], coords[i][0]);
+ var e = new L.LatLng(coords[i ? i - 1 : 0][1], coords[i ? i - 1 : 0][0]);
+ var newdist = opts.imperial ? s.distanceTo(e) * this.__mileFactor : s.distanceTo(e);
+ dist = dist + Math.round(newdist / 1000 * 100000) / 100000;
+
+ // skip point if it has not elevation
+ if (typeof coords[i][2] !== "undefined") {
+ ele = ele < coords[i][2] ? coords[i][2] : ele;
+ data.push({
+ dist: dist,
+ altitude: opts.imperial ? coords[i][2] * this.__footFactor : coords[i][2],
+ x: coords[i][0],
+ y: coords[i][1],
+ latlng: s
+ });
+ }
+ }
+ this._dist = dist;
+ this._data = data;
+ ele = opts.imperial ? ele * this.__footFactor : ele;
+ this._maxElevation = ele;
+ }
+ },
+
+ /*
+ * Parsing function for GPX data as used by https://github.com/mpetazzoni/leaflet-gpx
+ */
+ _addGPXdata: function(coords) {
+ var opts = this.options;
+ if (coords) {
+ var data = this._data || [];
+ var dist = this._dist || 0;
+ var ele = this._maxElevation || 0;
+ for (var i = 0; i < coords.length; i++) {
+ var s = coords[i];
+ var e = coords[i ? i - 1 : 0];
+ var newdist = opts.imperial ? s.distanceTo(e) * this.__mileFactor : s.distanceTo(e);
+ dist = dist + Math.round(newdist / 1000 * 100000) / 100000;
+ // skip point if it has not elevation
+ if (typeof s.meta.ele !== "undefined") {
+ ele = ele < s.meta.ele ? s.meta.ele : ele;
+ data.push({
+ dist: dist,
+ altitude: opts.imperial ? s.meta.ele * this.__footFactor : s.meta.ele,
+ x: s.lng,
+ y: s.lat,
+ latlng: s
+ });
+ }
+ }
+ this._dist = dist;
+ this._data = data;
+ ele = opts.imperial ? ele * this.__footFactor : ele;
+ this._maxElevation = ele;
+ }
+ },
+
+ _addData: function(d) {
+ var geom = d && d.geometry && d.geometry;
+ var i;
+
+ if (geom) {
+ switch (geom.type) {
+ case 'LineString':
+ this._addGeoJSONData(geom.coordinates);
+ break;
+
+ case 'MultiLineString':
+ for (i = 0; i < geom.coordinates.length; i++) {
+ this._addGeoJSONData(geom.coordinates[i]);
+ }
+ break;
+
+ default:
+ throw new Error('Invalid GeoJSON object.');
+ }
+ }
+
+ var feat = d && d.type === "FeatureCollection";
+ if (feat) {
+ for (i = 0; i < d.features.length; i++) {
+ this._addData(d.features[i]);
+ }
+ }
+
+ if (d && d._latlngs) {
+ this._addGPXdata(d._latlngs);
+ }
+ },
+
+ /*
+ * Calculates the full extent of the data array
+ */
+ _calculateFullExtent: function(data) {
+ if (!data || data.length < 1) {
+ throw new Error("no data in parameters");
+ }
+
+ var ext = new L.latLngBounds(data[0].latlng, data[0].latlng);
+
+ data.forEach(function(item) {
+ ext.extend(item.latlng);
+ });
+
+ return ext;
+ },
+
+ _getLineHexColor: function() {
+ var themeName = this.options.theme;
+ // TODO: find a better workaround for: this.gpx.setStyle({className: themeName});
+ var themeColor = themeName.substring(0, themeName.indexOf("-theme"));
+ var hexColor;
+ switch (themeColor) {
+ case 'lime':
+ hexColor = '#566B13';
+ break;
+ case 'steelblue':
+ hexColor = '#4682B4';
+ break;
+ case 'purple':
+ hexColor = '#732C7B';
+ break;
+ case 'magenta':
+ hexColor = '#FF005E';
+ break;
+ default:
+ hexColor = '#566B13';
+ break;
+ }
+ return hexColor;
+ },
+
+ _attachMapListeners: function() {
+ this._map.on('resize', function(e) {
+ var mapDiv = this._map.getContainer();
+ var eleDiv = document.querySelector(this.options.elevationDiv);
+
+ var newWidth = eleDiv.offsetWidth; // - 20;
+
+ if (newWidth <= 0) return;
+
+ this.options.width = newWidth;
+
+ eleDiv.innerHTML = "";
+
+ var container = this.onAdd(this._map);
+ container.classList.add("leaflet-control");
+
+ if (this.options.detachedView) {
+ eleDiv.appendChild(container);
+ } else {
+ this.addTo(this._map);
+ }
+ }, this);
+ },
+
+ /*
+ * Add data to the diagram either from GPX or GeoJSON and
+ * update the axis domain and data
+ */
+ addData: function(d, layer) {
+ this._addData(d);
+ if (this._container) {
+ this._applyData();
+ }
+ if (layer === null && d.on) {
+ layer = d;
+ }
+ if (layer) {
+ layer
+ .on("mousemove", this._handleLayerMouseOver.bind(this))
+ .on("mouseout", this._mouseoutHandler.bind(this));
+ }
+ },
+
+ addGPXFile: function(url) {
+ if (!url) {
+ throw "Invalid gpx url";
+ }
+
+ this.options.gpxOptions.polyline_options.color = this._getLineHexColor();
+
+ this.gpx = new L.GPX(url, this.options.gpxOptions);
+
+ this.gpx.on('loaded', function(e) {
+ this._map.fitBounds(e.target.getBounds());
+ });
+ this.gpx.once("addline", function(e) {
+ this.addData(e.line, this.gpx);
+ that._map.fireEvent("eledata_loaded", {
+ data: null,
+ layer: this.gpx
+ }, true);
+ }, this);
+
+ this.gpx.addTo(this._map);
+
+ this._attachMapListeners();
+
+ },
+
+ addGeoJSONFile: function(url) {
+ if (!url) {
+ throw "Invalid gpx url";
+ }
+
+ var hexColor = this._getLineHexColor();
+
+ var that = this;
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.setRequestHeader('Content-Type', 'application/json');
+ xhr.responseType = 'json';
+ xhr.onload = function() {
+
+ if (xhr.status !== 200) return;
+
+ that.geojson = L.geoJson(xhr.response, {
+ style: function(feature) {
+ return {
+ color: that._getLineHexColor()
+ };
+ },
+ onEachFeature: that.addData.bind(that),
+ });
+
+ that._map.once('layeradd', function(e) {
+ that._map.fitBounds(that.geojson.getBounds());
+ that._map.fireEvent("eledata_loaded", {
+ data: xhr.response,
+ layer: that.geojson
+ }, true);
+ });
+
+ that.geojson.addTo(that._map);
+
+ };
+ xhr.send();
+
+ this._attachMapListeners();
+ },
+
+ /*
+ * Handles mouseover events of the data layers on the map.
+ */
+ _handleLayerMouseOver: function(evt) {
+ if (!this._data || this._data.length === 0) {
+ return;
+ }
+ var latlng = evt.latlng;
+ var item = this._findItemForLatLng(latlng);
+ if (item) {
+ var x = item.xDiagCoord;
+
+ this._forceHidePositionMarker();
+ this._showDiagramIndicator(item, x);
+ this._showPositionMarker(item);
+ }
+ },
+
+ _showDiagramIndicator: function(item, xCoordinate) {
+ var opts = this.options;
+ this._focusG.style("visibility", "visible");
+ this._mousefocus.attr('x1', xCoordinate)
+ .attr('y1', 0)
+ .attr('x2', xCoordinate)
+ .attr('y2', this._height())
+ .classed('hidden', false);
+
+ var alt = item.altitude,
+ dist = item.dist,
+ ll = item.latlng,
+ numY = opts.hoverNumber.formatter(alt, opts.hoverNumber.decimalsY),
+ numX = opts.hoverNumber.formatter(dist, opts.hoverNumber.decimalsX);
+
+ this._focuslabeltext
+ .attr("x", xCoordinate)
+ .attr("y", this._y(item.altitude))
+ .style("font-weight", "700");
+
+ this._focuslabelX
+ .text(numY + (opts.imperial ? " ft" : " m"))
+ .attr("x", xCoordinate + 10);
+ this._focuslabelY
+ .text(numX + (opts.imperial ? " mi" : " km"))
+ .attr("x", xCoordinate + 10);
+
+ var bbox = this._focuslabeltext.node().getBBox();
+ var padding = 2;
+
+ this._focuslabelrect
+ .attr("x", bbox.x - padding)
+ .attr("y", bbox.y - padding)
+ .attr("width", bbox.width + (padding * 2))
+ .attr("height", bbox.height + (padding * 2));
+
+ },
+
+ _applyData: function() {
+ var xdomain = d3.extent(this._data, function(d) {
+ return d.dist;
+ });
+ var ydomain = d3.extent(this._data, function(d) {
+ return d.altitude;
+ });
+ var opts = this.options;
+
+ if (opts.yAxisMin !== undefined && (opts.yAxisMin < ydomain[0] || opts.forceAxisBounds)) {
+ ydomain[0] = opts.yAxisMin;
+ }
+ if (opts.yAxisMax !== undefined && (opts.yAxisMax > ydomain[1] || opts.forceAxisBounds)) {
+ ydomain[1] = opts.yAxisMax;
+ }
+
+ this._x.domain(xdomain);
+ this._y.domain(ydomain);
+ this._areapath.datum(this._data)
+ .attr("d", this._area);
+ this._updateAxis();
+
+ this._fullExtent = this._calculateFullExtent(this._data);
+ },
+
+ /*
+ * Reset data
+ */
+ _clearData: function() {
+ this._data = null;
+ this._dist = null;
+ this._maxElevation = null;
+ // if (this.gpx) {
+ // this.gpx.removeFrom(this._map);
+ // }
+ },
+
+ /*
+ * Reset data and display
+ */
+ clear: function() {
+ this._clearData();
+
+ if (!this._areapath) {
+ return;
+ }
+
+ // workaround for 'Error: Problem parsing d=""' in Webkit when empty data
+ // https://groups.google.com/d/msg/d3-js/7rFxpXKXFhI/HzIO_NPeDuMJ
+ //this._areapath.datum(this._data).attr("d", this._area);
+ this._areapath.attr("d", "M0 0");
+
+ this._x.domain([0, 1]);
+ this._y.domain([0, 1]);
+ this._updateAxis();
+ },
+
+ hide: function() {
+ this._container.style.display = "none";
+ },
+
+ show: function() {
+ this._container.style.display = "block";
+ },
+
+ _addToElevationDiv: function(map) {
+ var container = this.onAdd(map);
+ container.classList.add("leaflet-control");
+ var eleDiv = document.querySelector(this.options.elevationDiv);
+ eleDiv.appendChild(container);
+ },
+
+ loadChart: function(map) {
if (this.options.detachedView) {
- this._addToElevationDiv(map);
- } else {
- this.addTo(map);
- }
+ this._addToElevationDiv(map);
+ } else {
+ this.addTo(map);
+ }
+ },
+
+ loadGPX: function(map, url) {
+ this.loadChart(map);
+ this.addGPXFile(url);
},
- loadGPX: function(map, url) {
- this.loadChart(map);
- this.addGPXFile(url);
- }
+ loadGeoJSON: function(map, url) {
+ this.loadChart(map);
+ this.addGeoJSONFile(url);
+ }
});
L.control.elevation = function(options) {
- return new L.Control.Elevation(options);
+ return new L.Control.Elevation(options);
};