fixes leaflet-elevation in build
This commit is contained in:
783
web/static/vendor/leaflet-elevation/src/components/chart.js
vendored
Normal file
783
web/static/vendor/leaflet-elevation/src/components/chart.js
vendored
Normal file
@@ -0,0 +1,783 @@
|
||||
import * as D3 from './d3.js';
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
export var Chart = L.Control.Elevation.Chart = L.Class.extend({
|
||||
|
||||
includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
|
||||
|
||||
initialize(opts, control) {
|
||||
this.options = opts;
|
||||
this.control = control;
|
||||
|
||||
this._data = control._data || [];
|
||||
|
||||
// cache registered components
|
||||
this._props = {
|
||||
scales : {},
|
||||
paths : {},
|
||||
areas : {},
|
||||
grids : {},
|
||||
axes : {},
|
||||
legendItems : {},
|
||||
tooltipItems: {},
|
||||
};
|
||||
|
||||
this._scales = {};
|
||||
this._domains = {};
|
||||
this._ranges = {};
|
||||
this._paths = {};
|
||||
|
||||
this._brushEnabled = opts.dragging;
|
||||
this._zoomEnabled = opts.zooming;
|
||||
|
||||
opts.xTicks = this._xTicks();
|
||||
opts.yTicks = this._yTicks();
|
||||
|
||||
let chart = this._chart = D3.Chart(opts);
|
||||
|
||||
// SVG Container
|
||||
this._container = chart.svg;
|
||||
|
||||
// Panes
|
||||
this._grid = chart.pane('grid');
|
||||
this._area = chart.pane('area');
|
||||
this._point = chart.pane('point');
|
||||
this._axis = chart.pane('axis');
|
||||
this._legend = chart.pane('legend');
|
||||
this._tooltip = chart.pane('tooltip');
|
||||
this._ruler = chart.pane('ruler');
|
||||
|
||||
// Scales
|
||||
this._initScale();
|
||||
|
||||
// Helpers
|
||||
this._mask = chart.get('mask');
|
||||
this._context = chart.get('context');
|
||||
this._brush = chart.get('brush');
|
||||
|
||||
this._zoom = d3.zoom();
|
||||
this._drag = d3.drag();
|
||||
|
||||
// Interactions
|
||||
this._initInteractions();
|
||||
|
||||
// svg.on('resize', (e)=>console.log(e.detail));
|
||||
|
||||
// Handle multi-track segments
|
||||
this._maskGaps = [];
|
||||
control.on('eletrack_added', ({index}) => {
|
||||
this._maskGaps.push(index);
|
||||
control.once('elepoint_added', ({index}) => this._maskGaps.push(index));
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
update(props) {
|
||||
if (props) {
|
||||
if (props.data) this._data = props.data;
|
||||
if (props.options) this.options = props.options;
|
||||
}
|
||||
|
||||
this.options.xTicks = this._xTicks();
|
||||
this.options.yTicks = this._yTicks();
|
||||
|
||||
this._updateScale();
|
||||
this._updateAxis();
|
||||
this._updateMargins();
|
||||
this._updateLegend();
|
||||
this._updateClipper();
|
||||
this._updateArea();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
render() {
|
||||
return container => container.append(() => this._container.node());
|
||||
},
|
||||
|
||||
clear() {
|
||||
this._resetDrag();
|
||||
this._hideDiagramIndicator()
|
||||
this._area.selectAll('path').attr("d", "M0 0");
|
||||
this._context.clearRect(0, 0, this._width(), this._height());
|
||||
this._mask.selectAll(".gap").remove();
|
||||
this._maskGaps = [];
|
||||
|
||||
// if (this._path) {
|
||||
// this._x.domain([0, 1]);
|
||||
// this._y.domain([0, 1]);
|
||||
// }
|
||||
},
|
||||
|
||||
_drawPath(name) {
|
||||
let path = this._paths[name];
|
||||
let area = this._props.areas[name];
|
||||
|
||||
path.datum(this._data).attr("d",
|
||||
D3.Area(
|
||||
L.extend({}, area, {
|
||||
width : this._width(),
|
||||
height : this._height(),
|
||||
scaleX : this._scales[area.scaleX],
|
||||
scaleY : this._scales[area.scaleY]
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (!path.classed('leaflet-hidden')) {
|
||||
this.options.preferCanvas
|
||||
? _.drawCanvas(this._context, path)
|
||||
: _.append(this._area.node(), path.node());
|
||||
}
|
||||
},
|
||||
|
||||
_hasActiveLayers() {
|
||||
const paths = this._paths;
|
||||
for (var i in paths) {
|
||||
if (!paths[i].classed('leaflet-hidden')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize "d3-brush".
|
||||
*/
|
||||
_initBrush(e) {
|
||||
const brush = ({selection}) => {
|
||||
if (selection && this._data.length) {
|
||||
let start = this._findIndexForXCoord(selection[0]);
|
||||
let end = this._findIndexForXCoord(selection[1]);
|
||||
this.fire('dragged', { dragstart: this._data[start], dragend: this._data[end] });
|
||||
}
|
||||
}
|
||||
|
||||
const focus = (e) => {
|
||||
if (this._data.length && (e.type != 'brush' /*|| e.sourceEvent*/)) {
|
||||
let rect = this._chart.panes.brush.select('.overlay').node();
|
||||
let coords = d3.pointers(e, rect)[0];
|
||||
let xCoord = coords[0];
|
||||
let item = this._data[this._findIndexForXCoord(xCoord)];
|
||||
|
||||
this.fire("mouse_move", { item: item, xCoord: xCoord });
|
||||
}
|
||||
};
|
||||
|
||||
this._brush
|
||||
.filter(({shiftKey, button}) => !shiftKey && !button && this._brushEnabled)
|
||||
.on("end.update", brush)
|
||||
.on("brush.update", focus);
|
||||
|
||||
this._chart.panes.brush
|
||||
.on("mouseenter.focus touchstart.focus", this.fire.bind(this, "mouse_enter"))
|
||||
.on("mouseout.focus touchend.focus", this.fire.bind(this, "mouse_out") )
|
||||
.on("mousemove.focus touchmove.focus", focus );
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize "d3-zoom"
|
||||
*/
|
||||
_initClipper() {
|
||||
let svg = this._container;
|
||||
let margin = this.options.margins;
|
||||
|
||||
const zoom = this._zoom;
|
||||
|
||||
const onStart = ({transform, sourceEvent}) => {
|
||||
if (sourceEvent && sourceEvent.type == "mousedown") svg.style('cursor', 'grabbing');
|
||||
if (transform.k == 1 && transform.x == 0) {
|
||||
this._container.classed('zoomed', true);
|
||||
// Apply d3-zoom and bind <mask>
|
||||
if (this._mask) {
|
||||
this._point.attr('mask', 'url(#' + this._mask.attr('id') + ')');
|
||||
}
|
||||
}
|
||||
this.zooming = true;
|
||||
};
|
||||
|
||||
const onEnd = ({transform}) => {
|
||||
if (transform.k ==1 && transform.x == 0) {
|
||||
this._container.classed('zoomed', false);
|
||||
// Reset d3-zoom and remove <mask>
|
||||
if (this._mask) {
|
||||
this._point.attr('mask', null);
|
||||
}
|
||||
}
|
||||
this.zooming = false;
|
||||
svg.style('cursor', '');
|
||||
};
|
||||
|
||||
const onZoom = ({transform, sourceEvent}) => {
|
||||
// TODO: find a faster way to redraw the chart.
|
||||
this.zooming = false;
|
||||
this._updateScale(); // hacky way for restoring x scale when zooming out
|
||||
this.zooming = true;
|
||||
this._scales.distance = this._x = transform.rescaleX(this._x); // calculate x scale at zoom level
|
||||
if (this._scales.time) this._scales.time = transform.rescaleX(this._scales.time); // calculate x scale at zoom level
|
||||
this._resetDrag();
|
||||
if (sourceEvent && sourceEvent.type == "mousemove") {
|
||||
this._hideDiagramIndicator();
|
||||
}
|
||||
this.fire('zoom');
|
||||
};
|
||||
|
||||
zoom
|
||||
.scaleExtent([1, 10])
|
||||
.extent([
|
||||
[margin.left, 0],
|
||||
[this._width() - margin.right, this._height()]
|
||||
])
|
||||
.translateExtent([
|
||||
[margin.left, -Infinity],
|
||||
[this._width() - margin.right, Infinity]
|
||||
])
|
||||
.filter(({shiftKey, buttons}) => (shiftKey || buttons == 4) && this._zoomEnabled)
|
||||
.on("start", onStart)
|
||||
.on("end", onEnd)
|
||||
.on("zoom", onZoom);
|
||||
|
||||
svg.call(zoom); // add zoom functionality to "svg" group
|
||||
|
||||
// d3.select("body").on("keydown.grabzoom keyup.grabzoom", (e) => svg.style('cursor', e.shiftKey ? 'move' : ''));
|
||||
|
||||
},
|
||||
|
||||
_initInteractions() {
|
||||
this._initBrush();
|
||||
this._initRuler();
|
||||
this._initClipper();
|
||||
this._initLegend();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle chart data on legend click
|
||||
*/
|
||||
_initLegend() {
|
||||
this._container.on('legend_clicked', ({detail}) => {
|
||||
let { path, legend, name, enabled } = detail;
|
||||
if (path) {
|
||||
let label = _.select('text', legend);
|
||||
let rect = _.select('rect', legend);
|
||||
_.toggleStyle(label, 'text-decoration-line', 'line-through', enabled);
|
||||
_.toggleStyle(rect, 'fill-opacity', '0', enabled);
|
||||
_.toggleClass(path, 'leaflet-hidden', enabled);
|
||||
this._updateArea();
|
||||
this.fire("elepath_toggle", { path, name, legend, enabled })
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize "ruler".
|
||||
*/
|
||||
_initRuler() {
|
||||
if (!this.options.ruler) return;
|
||||
|
||||
// const yMax = this._height();
|
||||
const formatNum = d3.format(".0f");
|
||||
const drag = this._drag;
|
||||
|
||||
const label = (e, d) => {
|
||||
let yMax = this._height();
|
||||
let y = this._ruler.data()[0].y;
|
||||
if (y >= yMax || y <= 0) this._ruler.select(".horizontal-drag-label").text('');
|
||||
this._hideDiagramIndicator();
|
||||
};
|
||||
|
||||
const position = (e, d) => {
|
||||
let yCoord = d3.pointers(e, this._area.node())[0][1];
|
||||
let yMax = this._height();
|
||||
let y = _.clamp(yCoord, [0, yMax]);
|
||||
|
||||
this._ruler
|
||||
.data([L.extend(this._ruler.data()[0], { y: y })])
|
||||
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
|
||||
.classed('active', y < yMax);
|
||||
|
||||
this._container
|
||||
.select(".horizontal-drag-label")
|
||||
.text(formatNum(this._y.invert(y)) + " " + (this.options.imperial ? 'ft' : 'm'));
|
||||
|
||||
this.fire('ruler_filter', { coords: yCoord < yMax && yCoord > 0 ? this._findCoordsForY(yCoord) : [] });
|
||||
}
|
||||
|
||||
drag
|
||||
.on("start end", label)
|
||||
.on("drag", position);
|
||||
|
||||
this._ruler.call(drag);
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize x and y scales
|
||||
*/
|
||||
_initScale() {
|
||||
let opts = this.options;
|
||||
|
||||
this._registerAxisScale({
|
||||
axis : 'x',
|
||||
position: 'bottom',
|
||||
attr : opts.xAttr,
|
||||
min : opts.xAxisMin,
|
||||
max : opts.xAxisMax,
|
||||
name : 'distance'
|
||||
});
|
||||
|
||||
this._registerAxisScale({
|
||||
axis : 'y',
|
||||
position : 'left',
|
||||
attr : opts.yAttr,
|
||||
min : opts.yAxisMin,
|
||||
max : opts.yAxisMax,
|
||||
name : 'altitude'
|
||||
});
|
||||
|
||||
this._x = this._scales.distance;
|
||||
this._y = this._scales.altitude;
|
||||
},
|
||||
|
||||
_registerAreaPath(props) {
|
||||
if (props.scale == 'y') props.scale = this._y;
|
||||
else if (props.scale == 'x') props.scale = this._x;
|
||||
|
||||
let opts = this.options;
|
||||
|
||||
if (!props.xAttr) props.xAttr = opts.xAttr;
|
||||
if (!props.yAttr) props.yAttr = opts.yAttr;
|
||||
if (typeof props.preferCanvas === "undefined") props.preferCanvas = opts.preferCanvas;
|
||||
|
||||
// Save paths in memory for latter usage
|
||||
this._paths[props.name] = D3.Path(props);
|
||||
this._props.areas[props.name] = props;
|
||||
|
||||
if (opts.legend) {
|
||||
this._props.legendItems[props.name] = {
|
||||
name : props.name,
|
||||
label : props.label,
|
||||
color : props.color,
|
||||
className: props.className,
|
||||
path : this._paths[props.name]
|
||||
};
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
_registerAxisGrid(props) {
|
||||
if (props.scale == 'y') props.scale = this._y;
|
||||
else if (props.scale == 'x') props.scale = this._x;
|
||||
|
||||
this._props.grids[props.name || props.axis] = props;
|
||||
},
|
||||
|
||||
_registerAxisScale(props) {
|
||||
if (props.scale == 'y') props.scale = this._y;
|
||||
else if (props.scale == 'x') props.scale = this._x;
|
||||
|
||||
let opts = this.options;
|
||||
let scale = props.scale;
|
||||
|
||||
if (typeof this._scales[props.name] === 'function') {
|
||||
props.scale = this._scales[props.name]; // retrieve cached scale
|
||||
} else if (typeof scale !== 'function') {
|
||||
scale = L.extend({
|
||||
data : this._data,
|
||||
forceBounds: opts.forceAxisBounds
|
||||
}, scale);
|
||||
|
||||
scale.attr = scale.attr || props.name;
|
||||
|
||||
let domain = this._domains[props.name] = D3.Domain(props);
|
||||
let range = this._ranges[props.name] = D3.Range(props);
|
||||
scale.range = scale.range || range(this._width(),this._height());
|
||||
scale.domain = scale.domain || domain(this._data);
|
||||
|
||||
this._props.scales[props.name] = scale;
|
||||
|
||||
props.scale = this._scales[props.name] = D3.Scale(scale);
|
||||
}
|
||||
|
||||
if (!props.ticks) {
|
||||
if (props.axis == 'x') props.ticks = this._xTicks.bind(this);
|
||||
else if (props.axis == 'y') props.ticks = this._yTicks.bind(this);
|
||||
}
|
||||
|
||||
this._props.axes[props.name] = props;
|
||||
|
||||
if (props.name == this.options.yScale) this._y = scale;
|
||||
if (props.name == this.options.xScale) this._x = scale;
|
||||
|
||||
return scale;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a point of interest over the chart
|
||||
*/
|
||||
_registerCheckPoint(point) {
|
||||
if (!this._data.length) return;
|
||||
|
||||
const {xAttr, yAttr} = this.options;
|
||||
let item, x, y;
|
||||
|
||||
if (point.latlng) {
|
||||
item = this._data[this._findIndexForLatLng(point.latlng)];
|
||||
x = this._x(item[xAttr]);
|
||||
y = this._y(item[yAttr]);
|
||||
} else if (!isNaN(point[xAttr])) {
|
||||
x = this._x(point[xAttr]);
|
||||
item = this._data[this._findIndexForXCoord(x)]
|
||||
y = this._y(item[yAttr]);
|
||||
}
|
||||
|
||||
this._point.call(D3.CheckPoint({
|
||||
point: point,
|
||||
width: this._width(),
|
||||
height: this._height(),
|
||||
x: x,
|
||||
y: y,
|
||||
}));
|
||||
},
|
||||
|
||||
_registerTooltip(props) {
|
||||
props.order = props.order ?? 1000;
|
||||
this._props.tooltipItems[props.name] = props;
|
||||
},
|
||||
|
||||
_updateArea() {
|
||||
// Reset and update chart profiles
|
||||
this._context.clearRect(0, 0, this._width(), this._height());
|
||||
_.each(this._paths, (path, i) => !path.classed('leaflet-hidden') && this._drawPath(i));
|
||||
},
|
||||
|
||||
_updateAxis() {
|
||||
|
||||
let opts = this.options;
|
||||
|
||||
const gridOpts = {
|
||||
width : this._width(),
|
||||
height : this._height(),
|
||||
tickFormat: "",
|
||||
};
|
||||
|
||||
const axesOpts = {
|
||||
width : this._width(),
|
||||
height : this._height(),
|
||||
};
|
||||
|
||||
// Reset grids
|
||||
this._grid.selectAll('g').remove();
|
||||
_.each(this._props.grids, (grid, i) => {
|
||||
if (opts[i] !== false && opts[i] !== 'summary') {
|
||||
this._grid.call(D3.Grid(L.extend({}, gridOpts, grid)))
|
||||
}
|
||||
});
|
||||
|
||||
// Rest axis
|
||||
this._axis.selectAll('g').remove();
|
||||
_.each(this._props.axes, (axis, i) => {
|
||||
if (opts[i] !== false && opts[i] !== 'summary') {
|
||||
this._axis.call(D3.Axis(L.extend({}, axesOpts, axis)));
|
||||
}
|
||||
});
|
||||
|
||||
// Adjust axis scale positions
|
||||
this._axis
|
||||
.selectAll('.y.axis.right')
|
||||
.each((d, i, n) => {
|
||||
|
||||
let axis = d3.select(n[i]);
|
||||
let transform = axis.attr('transform');
|
||||
let translate = transform.substring(transform.indexOf("(") + 1, transform.indexOf(")")).split(",");
|
||||
|
||||
axis.attr('transform', 'translate(' + (+translate[0] + (i * 40)) + ',' + translate[1] + ')')
|
||||
|
||||
if (i > 0) {
|
||||
axis.select(':scope > path') .attr('opacity', 0.25);
|
||||
axis.selectAll(':scope > .tick line').attr('opacity', 0.75);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
_updateClipper() {
|
||||
const { xAttr, margins } = this.options;
|
||||
const data = this._data;
|
||||
|
||||
this._zoom
|
||||
.scaleExtent([1, 10])
|
||||
.extent([
|
||||
[margins.left, 0],
|
||||
[this._width() - margins.right, this._height()]
|
||||
])
|
||||
.translateExtent([
|
||||
[margins.left, -Infinity],
|
||||
[this._width() - margins.right, Infinity]
|
||||
]);
|
||||
|
||||
// Apply svg mask on multi-track segments
|
||||
this._mask.selectAll(".gap").remove()
|
||||
this._maskGaps.forEach((d, i) => {
|
||||
if (i >= this._maskGaps.length - 2) return;
|
||||
let x1 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i]].latlng)][xAttr]);
|
||||
let x2 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i + 1]].latlng)][xAttr]);
|
||||
this._mask
|
||||
.append("rect")
|
||||
.attr("x", x1)
|
||||
.attr("y", 0)
|
||||
.attr("width", x2 - x1 )
|
||||
.attr("height", this._height())
|
||||
.attr('class', 'gap')
|
||||
.attr('fill-opacity', '0.8')
|
||||
.attr("fill", 'black'); // hide = black (mask)
|
||||
});
|
||||
},
|
||||
|
||||
_updateLegend: function () {
|
||||
let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
|
||||
|
||||
// Get legend items
|
||||
let items = Object.keys(this._paths);
|
||||
|
||||
// Calculate legend item positions
|
||||
let n = items.length;
|
||||
let v = Array(Math.floor(n / 2)).fill(null).map((d, i) => (i + 1) * 2 - (1 - Math.sign(n % 2)));
|
||||
let rev = v.slice().reverse().map((d) => -(d));
|
||||
|
||||
// push a fake element to handle center alignment (odd numbers)
|
||||
if (n % 2 !== 0) {
|
||||
rev.push(0);
|
||||
}
|
||||
v = rev.concat(v);
|
||||
|
||||
// Reset legend items
|
||||
this._legend.selectAll('g').remove();
|
||||
|
||||
// Render legend items
|
||||
_.each(this._props.legendItems, (legend) => {
|
||||
this._legend.append("g").call(
|
||||
D3.LegendItem(L.extend({
|
||||
width : this._width(),
|
||||
height : this._height(),
|
||||
margins: this.options.margins,
|
||||
}, legend))
|
||||
);
|
||||
});
|
||||
|
||||
// Render legend item switcher
|
||||
if (n > 1) {
|
||||
this._legend.append("g").call(
|
||||
D3.LegendSmall({
|
||||
width : this._width(),
|
||||
height : this._height(),
|
||||
items : items,
|
||||
onClick: (selected) => {
|
||||
_.each(items, name => this._togglePath(name, selected == name, true));
|
||||
this._updateArea();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_.each(items, (name, i) => {
|
||||
// Adjust legend item positions
|
||||
this._legend.select('[data-name=' + name + ']').attr("transform", "translate(" + (v[i] * 55) + ", " + (xAxesB * 2) + ")");
|
||||
// Set initial state (disabled controls)
|
||||
this._togglePath(name, !(name in this.options && this.options[name] == 'disabled'), true);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
_updateMargins() {
|
||||
// Get chart margins
|
||||
let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
|
||||
let xAxesL = this._axis.selectAll('.y.axis.left').nodes().length;
|
||||
let xAxesR = this._axis.selectAll('.y.axis.right').nodes().length;
|
||||
let marginB = 60 + (xAxesB * 2);
|
||||
let marginL = 10 + (xAxesL * 30);
|
||||
let marginR = 40 + (xAxesR * 30);
|
||||
|
||||
let marginsUpdated = false
|
||||
|
||||
// Adjust right margin
|
||||
if (xAxesR && this.options.margins.right < marginR) {
|
||||
this.options.margins.right = marginR;
|
||||
marginsUpdated = true;
|
||||
}
|
||||
|
||||
// Adjust left margin
|
||||
if (xAxesL && this.options.margins.left < marginL) {
|
||||
this.options.margins.left = marginL;
|
||||
marginsUpdated = true;
|
||||
}
|
||||
|
||||
// Adjust bottom margin
|
||||
if (xAxesB && this.options.margins.bottom < marginB) {
|
||||
this.options.margins.bottom = marginB;
|
||||
marginsUpdated = true;
|
||||
}
|
||||
|
||||
if (marginsUpdated) {
|
||||
this.fire('margins_updated');
|
||||
}
|
||||
},
|
||||
|
||||
_updateScale() {
|
||||
if (this.zooming) return { x: this._x, y: this._y };
|
||||
|
||||
for (let i in this._scales) {
|
||||
this._scales[i]
|
||||
.domain(this._domains[i](this._data))
|
||||
.range(this._ranges[i](this._width(), this._height()))
|
||||
}
|
||||
|
||||
return { x: this._x, y: this._y };
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates chart width.
|
||||
*/
|
||||
_width() {
|
||||
if (this._chart) this._chart._width;
|
||||
const { width, margins } = this.options;
|
||||
return width - margins.left - margins.right;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates chart height.
|
||||
*/
|
||||
_height() {
|
||||
if (this._chart) return this._chart._height;
|
||||
const { height, margins } = this.options;
|
||||
return height - margins.top - margins.bottom;
|
||||
},
|
||||
|
||||
/*
|
||||
* Finds data entries above a given y-elevation value and returns geo-coordinates
|
||||
*/
|
||||
_findCoordsForY(y) {
|
||||
let data = this._data;
|
||||
let z = this._y.invert(y);
|
||||
|
||||
// save indexes of elevation values above the horizontal line
|
||||
const list = data.reduce((array, item, index) => {
|
||||
if (item[this.options.yAttr] >= z) array.push(index);
|
||||
return array;
|
||||
}, []);
|
||||
|
||||
let start = 0;
|
||||
let next;
|
||||
|
||||
// split index list into blocks of coordinates
|
||||
const coords = list.reduce((array, _, curr) => {
|
||||
next = curr + 1;
|
||||
if (list[next] !== list[curr] + 1 || next === list.length) {
|
||||
array.push(
|
||||
list
|
||||
.slice(start, next)
|
||||
.map(i => data[i].latlng)
|
||||
);
|
||||
start = next;
|
||||
}
|
||||
return array;
|
||||
}, []);
|
||||
|
||||
return coords;
|
||||
},
|
||||
|
||||
/*
|
||||
* Finds a data entry for a given x-coordinate of the diagram
|
||||
*/
|
||||
_findIndexForXCoord(x) {
|
||||
return d3
|
||||
.bisector(d => d[this.options.xAttr])
|
||||
.left(this._data || [0, 1], this._x.invert(x));
|
||||
},
|
||||
|
||||
/*
|
||||
* Finds a data entry for a given latlng of the map
|
||||
*/
|
||||
_findIndexForLatLng(latlng) {
|
||||
let result = null;
|
||||
let d = Infinity;
|
||||
this._data.forEach((item, index) => {
|
||||
let dist = latlng.distanceTo(item.latlng);
|
||||
if (dist < d) {
|
||||
d = dist;
|
||||
result = index;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
/*
|
||||
* Removes the drag rectangle and zoms back to the total extent of the data.
|
||||
*/
|
||||
_resetDrag() {
|
||||
if (this._chart.panes.brush.select(".selection").attr('width')) {
|
||||
this._chart.panes.brush.call(this._brush.clear);
|
||||
this._hideDiagramIndicator();
|
||||
this.fire('reset_drag');
|
||||
}
|
||||
},
|
||||
|
||||
_resetZoom() {
|
||||
if (this._zoom) {
|
||||
this._zoom.transform(this._chart.svg, d3.zoomIdentity);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display distance and altitude level ("focus-rect").
|
||||
*/
|
||||
_showDiagramIndicator(item, xCoordinate) {
|
||||
this._tooltip
|
||||
.attr("display", null)
|
||||
.call(D3.Tooltip({
|
||||
xCoord: xCoordinate,
|
||||
yCoord: this._y(item[this.options.yAttr]),
|
||||
height: this._height(),
|
||||
width : this._width(),
|
||||
labels: this._props.tooltipItems,
|
||||
item: item
|
||||
}));
|
||||
},
|
||||
|
||||
_togglePath(name, enabled = true, lazy = false) {
|
||||
let path = this._paths[name];
|
||||
let legend = this._container.select('.legend [data-name=' + name + ']');
|
||||
path.classed('leaflet-hidden', !enabled);
|
||||
legend.select('text').style('text-decoration-line', enabled ? '' : 'line-through');
|
||||
legend.select('rect').style('fill-opacity', enabled ? '': '0');
|
||||
// Apply d3-zoom (bind <clipPath> mask)
|
||||
if (this._mask) {
|
||||
path.attr('mask', 'url(#' + this._mask.attr('id') + ')');
|
||||
}
|
||||
if (!lazy) {
|
||||
this._updateArea();
|
||||
}
|
||||
},
|
||||
|
||||
_hideDiagramIndicator() {
|
||||
this._tooltip.attr("display", 'none');
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate chart xTicks
|
||||
*/
|
||||
_xTicks() {
|
||||
if (this.__xTicks) this.__xTicks = this.options.xTicks;
|
||||
return this.__xTicks || Math.round(this._width() / 75);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate chart yTicks
|
||||
*/
|
||||
_yTicks() {
|
||||
if (this.__yTicks) this.__yTicks = this.options.yTicks;
|
||||
return this.__yTicks || Math.round(this._height() / 30);
|
||||
}
|
||||
});
|
||||
649
web/static/vendor/leaflet-elevation/src/components/d3.js
vendored
Normal file
649
web/static/vendor/leaflet-elevation/src/components/d3.js
vendored
Normal file
@@ -0,0 +1,649 @@
|
||||
export const Area = ({
|
||||
width,
|
||||
height,
|
||||
xAttr,
|
||||
yAttr,
|
||||
scaleX,
|
||||
scaleY,
|
||||
interpolation = "curveLinear"
|
||||
}) => {
|
||||
return d3.area()
|
||||
.curve(typeof interpolation === 'string' ? d3[interpolation] : interpolation)
|
||||
.x(d => (d.xDiagCoord = scaleX(d[xAttr])))
|
||||
.y0(height)
|
||||
.y1(d => scaleY(d[yAttr]));
|
||||
};
|
||||
|
||||
export const Path = ({
|
||||
name,
|
||||
color,
|
||||
strokeColor,
|
||||
strokeOpacity,
|
||||
fillOpacity,
|
||||
className = ''
|
||||
}) => {
|
||||
let path = d3.create('svg:path')
|
||||
|
||||
if (name) path.classed(name + ' ' + className, true);
|
||||
|
||||
path.style("pointer-events", "none");
|
||||
|
||||
path
|
||||
.attr("fill", color || '#3366CC')
|
||||
.attr("stroke", strokeColor || '#000')
|
||||
.attr("stroke-opacity", strokeOpacity || '1')
|
||||
.attr("fill-opacity", fillOpacity || '0.8');
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const Axis = ({
|
||||
type = "axis",
|
||||
tickSize = 6,
|
||||
tickPadding = 3,
|
||||
position,
|
||||
height,
|
||||
width,
|
||||
axis,
|
||||
scale,
|
||||
ticks,
|
||||
tickFormat,
|
||||
label,
|
||||
labelX,
|
||||
labelY,
|
||||
name = "",
|
||||
onAxisMount,
|
||||
}) => {
|
||||
return g => {
|
||||
let [w, h] = [0, 0];
|
||||
if (position == "bottom") h = height;
|
||||
if (position == "right") w = width;
|
||||
|
||||
if (axis == "x" && type == "grid") {
|
||||
tickSize = -height;
|
||||
} else if (axis == "y" && type == "grid") {
|
||||
tickSize = -width;
|
||||
}
|
||||
|
||||
let axisScale = d3["axis" + position.replace(/\b\w/g, l => l.toUpperCase())]()
|
||||
.scale(scale)
|
||||
.ticks(typeof ticks === 'function' ? ticks() : ticks)
|
||||
.tickPadding(tickPadding)
|
||||
.tickSize(tickSize)
|
||||
.tickFormat(tickFormat);
|
||||
|
||||
let axisGroup = g.append("g")
|
||||
.attr("class", [axis, type, position, name].join(" "))
|
||||
.attr("transform", "translate(" + w + "," + h + ")")
|
||||
.call(axisScale);
|
||||
|
||||
if (label) {
|
||||
axisGroup.append("svg:text")
|
||||
.attr("x", labelX)
|
||||
.attr("y", labelY)
|
||||
.text(label);
|
||||
}
|
||||
|
||||
if (onAxisMount) {
|
||||
axisGroup.call(onAxisMount);
|
||||
}
|
||||
|
||||
return axisGroup;
|
||||
};
|
||||
};
|
||||
|
||||
export const Grid = (props) => {
|
||||
props.type = "grid";
|
||||
return Axis(props);
|
||||
};
|
||||
|
||||
export const PositionMarker = ({
|
||||
theme,
|
||||
xCoord = 0,
|
||||
yCoord = 0,
|
||||
labels = {},
|
||||
item = {},
|
||||
length = 0,
|
||||
}) => {
|
||||
return g => {
|
||||
|
||||
g.attr("class", "height-focus-group");
|
||||
|
||||
let line = g.select('.height-focus.line');
|
||||
let circle = g.select('.height-focus.circle-lower');
|
||||
let text = g.select('.height-focus-label');
|
||||
|
||||
if (!line.node()) line = g.append('svg:line');
|
||||
if (!circle.node()) circle = g.append('svg:circle');
|
||||
if (!text.node()) text = g.append('svg:text');
|
||||
|
||||
if (isNaN(xCoord) || isNaN(yCoord)) return g;
|
||||
|
||||
circle
|
||||
.attr("class", theme + " height-focus circle-lower")
|
||||
.attr("transform", "translate(" + xCoord + "," + yCoord + ")")
|
||||
.attr("r", 6)
|
||||
.attr("cx", 0)
|
||||
.attr("cy", 0);
|
||||
|
||||
line
|
||||
.attr("class", theme + " height-focus line")
|
||||
.attr("x1", xCoord)
|
||||
.attr("x2", xCoord)
|
||||
.attr("y1", yCoord)
|
||||
.attr("y2", length);
|
||||
|
||||
text
|
||||
.attr("class", theme + " height-focus-label")
|
||||
.style("pointer-events", "none")
|
||||
.attr("x", xCoord + 5)
|
||||
.attr("y", length);
|
||||
|
||||
let label;
|
||||
|
||||
Object
|
||||
.keys(labels)
|
||||
.sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
|
||||
.forEach((i)=> {
|
||||
label = text.select(".height-focus-" + labels[i].name);
|
||||
|
||||
if (!label.size()) {
|
||||
label = text.append("svg:tspan")
|
||||
.attr("class", "height-focus-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
|
||||
.attr("dy", "1.5em");
|
||||
}
|
||||
|
||||
label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
|
||||
|
||||
});
|
||||
|
||||
text.select('tspan').attr("dy", text.selectAll('tspan').size() > 1 ? "-1.5em" : "0em" );
|
||||
text.selectAll('tspan').attr("x", xCoord + 5);
|
||||
|
||||
return g;
|
||||
}
|
||||
};
|
||||
|
||||
export const LegendItem = ({
|
||||
name,
|
||||
label,
|
||||
width,
|
||||
height,
|
||||
margins = {},
|
||||
color,
|
||||
path,
|
||||
className = ''
|
||||
}) => {
|
||||
return g => {
|
||||
g
|
||||
.attr("class", "legend-item legend-" + name.toLowerCase())
|
||||
.attr("data-name", name);
|
||||
|
||||
g.on('click.legend', () => d3.select(g.node().ownerSVGElement || g)
|
||||
.dispatch("legend_clicked", {
|
||||
detail: {
|
||||
path: path.node(),
|
||||
name: name,
|
||||
legend: g.node(),
|
||||
enabled: !path.classed('leaflet-hidden'),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
g.append("svg:rect")
|
||||
.attr("x", (width / 2) - 50)
|
||||
.attr("y", height + margins.bottom / 2)
|
||||
.attr("width", 50)
|
||||
.attr("height", 10)
|
||||
.attr("fill", color)
|
||||
.attr("stroke", "#000")
|
||||
.attr("stroke-opacity", "0.5")
|
||||
.attr("fill-opacity", "0.25")
|
||||
.attr("class", className);
|
||||
|
||||
g.append('svg:text')
|
||||
.text(L._(label || name))
|
||||
.attr("x", (width / 2) + 5)
|
||||
.attr("font-size", 10)
|
||||
.style("font-weight", "700")
|
||||
.attr('y', height + margins.bottom / 2)
|
||||
.attr('dy', "0.75em");
|
||||
|
||||
return g;
|
||||
}
|
||||
};
|
||||
|
||||
export const LegendSmall = ({
|
||||
width,
|
||||
height,
|
||||
items,
|
||||
onClick
|
||||
}) => {
|
||||
return g => {
|
||||
let idx = 0;
|
||||
|
||||
g.data([{
|
||||
x: width,
|
||||
y: height + 40
|
||||
}]).attr("transform", d => "translate(" + d.x + "," + d.y + ")").classed('legend-switcher');
|
||||
|
||||
// let label = g.selectAll(".legend-switcher-label") .data([{ idx: 0 }]);
|
||||
let label = g.append('svg:text')
|
||||
.attr("class", "legend-switcher-label")
|
||||
.attr("text-anchor", "end")
|
||||
.attr("x", -25)
|
||||
.attr("y", 5)
|
||||
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
|
||||
|
||||
let symbol = g.selectAll(".legend-switcher-symbol").data([
|
||||
{ type: d3.symbolTriangle, x: 0, y: 3, angle: 0, size: 50, id: "down" },
|
||||
{ type: d3.symbolTriangle, x: -13, y: 0, angle: 180, size: 50, id: "up" }
|
||||
]);
|
||||
|
||||
symbol.exit().remove();
|
||||
label.exit().remove();
|
||||
|
||||
symbol.enter()
|
||||
.append("svg:path")
|
||||
.attr("class", "legend-switcher-symbol")
|
||||
.attr("cursor", 'pointer')
|
||||
.attr("fill", "#000")
|
||||
.merge(symbol)
|
||||
.attr("d",
|
||||
d3.symbol()
|
||||
.type(d => d.type)
|
||||
.size(d => d.size)
|
||||
)
|
||||
.attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")")
|
||||
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((d.id === "up" ? idx + 1 : idx - 1), [0, items.length])));
|
||||
|
||||
const setIdx = (id) => {
|
||||
idx = id;
|
||||
// label.enter()
|
||||
// .append('text')
|
||||
// .attr("class", "legend-switcher-label")
|
||||
// .attr("text-anchor", "end")
|
||||
// .attr("x", -25)
|
||||
// .attr("y", 5)
|
||||
// .merge(label.data([{ idx: id }]))
|
||||
// .text(d => items.length ? (items[idx][0].toUpperCase() + items[idx].slice(1)) : '')
|
||||
// .on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
|
||||
label.text(items.length ? L._((items[idx][0].toUpperCase() + items[idx].slice(1))) : '');
|
||||
onClick(items[idx]);
|
||||
};
|
||||
setIdx(0);
|
||||
|
||||
return g;
|
||||
};
|
||||
};
|
||||
|
||||
export const Tooltip = ({
|
||||
xCoord,
|
||||
yCoord,
|
||||
width,
|
||||
height,
|
||||
labels = {},
|
||||
item = {},
|
||||
}) => {
|
||||
return g => {
|
||||
|
||||
let line = g.select('.mouse-focus-line');
|
||||
let box = g.select('.mouse-focus-label');
|
||||
|
||||
if (!line.node()) line = g.append('svg:line');
|
||||
if (!box.node()) box = g.append("g");
|
||||
|
||||
let rect = box.select(".mouse-focus-label-rect");
|
||||
let text = box.select(".mouse-focus-label-text");
|
||||
|
||||
if (!rect.node()) rect = box.append("svg:rect");
|
||||
if (!text.node()) text = box.append("svg:text");
|
||||
|
||||
// Sets focus-label-text position to the left / right of the mouse-focus-line
|
||||
let xAlign = 0;
|
||||
let yAlign = 0;
|
||||
let bbox = { width: 0, height: 0 };
|
||||
try { bbox = text.node().getBBox(); } catch (e) { return g; }
|
||||
|
||||
if (xCoord) xAlign = xCoord + (xCoord < width / 2 ? 10 : -bbox.width - 10);
|
||||
if (yCoord) yAlign = Math.max(yCoord - bbox.height, L.Browser.webkit ? 0 : -Infinity);
|
||||
|
||||
line
|
||||
.attr('class', 'mouse-focus-line')
|
||||
.attr('x2', xCoord)
|
||||
.attr('y2', 0)
|
||||
.attr('x1', xCoord)
|
||||
.attr('y1', height);
|
||||
|
||||
box
|
||||
.attr('class', 'mouse-focus-label');
|
||||
|
||||
rect
|
||||
.attr("class", "mouse-focus-label-rect")
|
||||
.attr("x", xAlign - 5)
|
||||
.attr("y", yAlign - 5)
|
||||
.attr("width", bbox.width + 10)
|
||||
.attr("height", bbox.height + 10)
|
||||
.attr("rx", 3)
|
||||
.attr("ry", 3);
|
||||
|
||||
text
|
||||
.attr("class", "mouse-focus-label-text")
|
||||
.style("font-weight", "700")
|
||||
.attr("y", yAlign);
|
||||
|
||||
let label;
|
||||
|
||||
Object
|
||||
.keys(labels)
|
||||
.sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
|
||||
.forEach((i)=> {
|
||||
label = text.select(".mouse-focus-label-" + labels[i].name);
|
||||
|
||||
if (!label.size()) {
|
||||
label = text.append("svg:tspan", ".mouse-focus-label-x")
|
||||
.attr("class", "mouse-focus-label-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
|
||||
.attr("dy", "1.5em");
|
||||
}
|
||||
|
||||
label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
|
||||
});
|
||||
|
||||
text.select('tspan').attr("dy", "1em");
|
||||
text.selectAll('tspan').attr("x", xAlign);
|
||||
|
||||
return g;
|
||||
};
|
||||
};
|
||||
|
||||
export const Ruler = ({
|
||||
height,
|
||||
width,
|
||||
}) => {
|
||||
return g => {
|
||||
|
||||
g.data([{
|
||||
x: 0,
|
||||
y: height,
|
||||
}])
|
||||
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
|
||||
|
||||
let rect = g.selectAll('.horizontal-drag-rect').data([{ w: width }]);
|
||||
let line = g.selectAll('.horizontal-drag-line').data([{ w: width }]);
|
||||
let label = g.selectAll('.horizontal-drag-label').data([{ w: width - 8 }]);
|
||||
let symbol = g.selectAll('.horizontal-drag-symbol')
|
||||
.data([{
|
||||
type: d3.symbolTriangle,
|
||||
x: width + 7,
|
||||
y: 0,
|
||||
angle: -90,
|
||||
size: 50
|
||||
}]);
|
||||
|
||||
rect.exit().remove();
|
||||
line.exit().remove();
|
||||
label.exit().remove();
|
||||
symbol.exit().remove();
|
||||
|
||||
rect.enter()
|
||||
.append("svg:rect")
|
||||
.attr("class", "horizontal-drag-rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", -8)
|
||||
.attr("height", 8)
|
||||
.attr('fill', 'none')
|
||||
.attr('pointer-events', 'all')
|
||||
.merge(rect)
|
||||
.attr("width", d => d.w);
|
||||
|
||||
line.enter()
|
||||
.append("svg:line")
|
||||
.attr("class", "horizontal-drag-line")
|
||||
.attr("x1", 0)
|
||||
.merge(line)
|
||||
.attr("x2", d => d.w);
|
||||
|
||||
label.enter()
|
||||
.append("svg:text")
|
||||
.attr("class", "horizontal-drag-label")
|
||||
.attr("text-anchor", "end")
|
||||
.attr("y", -8)
|
||||
.merge(label)
|
||||
.attr("x", d => d.w)
|
||||
|
||||
symbol
|
||||
.enter()
|
||||
.append("svg:path")
|
||||
.attr("class", "horizontal-drag-symbol")
|
||||
.merge(symbol)
|
||||
.attr("d",
|
||||
d3.symbol()
|
||||
.type(d => d.type)
|
||||
.size(d => d.size)
|
||||
)
|
||||
.attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")");
|
||||
|
||||
return g;
|
||||
}
|
||||
};
|
||||
|
||||
export const CheckPoint = ({
|
||||
point,
|
||||
height,
|
||||
width,
|
||||
x,
|
||||
y
|
||||
}) => {
|
||||
return g => {
|
||||
|
||||
if (isNaN(x) || isNaN(y)) return g;
|
||||
|
||||
if (!point.item || !point.item.property('isConnected')) {
|
||||
point.position = point.position || "bottom";
|
||||
|
||||
point.item = g.append('g');
|
||||
|
||||
point.item.append("svg:line")
|
||||
.attr("y1", 0)
|
||||
.attr("x1", 0)
|
||||
.attr("style","stroke: rgb(51, 51, 51); stroke-width: 0.5; stroke-dasharray: 2, 2;");
|
||||
|
||||
point.item
|
||||
.append("svg:circle")
|
||||
.attr("class", " height-focus circle-lower")
|
||||
.attr("r", 3);
|
||||
|
||||
if (point.label) {
|
||||
point.item.append("svg:text")
|
||||
.attr("dx", "4px")
|
||||
.attr("dy", "-4px");
|
||||
}
|
||||
}
|
||||
|
||||
point.item
|
||||
.datum({
|
||||
pos: point.position,
|
||||
x: x,
|
||||
y: y
|
||||
})
|
||||
.attr("class", d => "point " + d.pos)
|
||||
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
|
||||
|
||||
point.item.select('line')
|
||||
.datum({
|
||||
y2: ({'top': -y, 'bottom': height - y})[point.position],
|
||||
x2: ({'left': -x, 'right': width - x})[point.position] || 0
|
||||
})
|
||||
.attr("y2", d => d.y2)
|
||||
.attr("x2", d => d.x2)
|
||||
|
||||
if (point.label) {
|
||||
point.item.select('text')
|
||||
.text(point.label);
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
};
|
||||
|
||||
export const Domain = ({
|
||||
min,
|
||||
max,
|
||||
attr,
|
||||
name,
|
||||
forceBounds,
|
||||
scale
|
||||
}) => function(data) {
|
||||
attr = (scale && scale.attr) || attr || name;
|
||||
let domain = data && data.length ? d3.extent(data, d => d[attr]) : [0, 1];
|
||||
if (typeof min !== "undefined" && (min < domain[0] || forceBounds)) {
|
||||
domain[0] = min;
|
||||
}
|
||||
if (typeof max !== "undefined" && (max > domain[1] || forceBounds)) {
|
||||
domain[1] = max;
|
||||
}
|
||||
return domain;
|
||||
};
|
||||
|
||||
export const Range = ({
|
||||
axis
|
||||
}) => function(width, height) {
|
||||
if (axis == 'x') return [0, width];
|
||||
else if (axis == 'y') return [height, 0];
|
||||
};
|
||||
|
||||
export const Scale = ({
|
||||
data,
|
||||
attr,
|
||||
min,
|
||||
max,
|
||||
forceBounds,
|
||||
range,
|
||||
}) => {
|
||||
return d3.scaleLinear()
|
||||
.range(range)
|
||||
.domain(Domain({min, max, attr, forceBounds})(data));
|
||||
};
|
||||
|
||||
export const Bisect = ({
|
||||
data = [0, 1],
|
||||
scale,
|
||||
x,
|
||||
attr
|
||||
}) => {
|
||||
return d3
|
||||
.bisector(d => d[attr])
|
||||
.left(data, scale.invert(x));
|
||||
};
|
||||
|
||||
export const Chart = ({
|
||||
width,
|
||||
height,
|
||||
margins = {},
|
||||
ruler,
|
||||
}) => {
|
||||
|
||||
const w = width - margins.left - margins.right;
|
||||
const h = height - margins.top - margins.bottom;
|
||||
|
||||
// SVG Container
|
||||
const svg = d3.create("svg:svg").attr("class", "background");
|
||||
|
||||
const defs = svg.append("svg:defs");
|
||||
|
||||
// SVG Groups
|
||||
const g = svg.append("g");
|
||||
const panes = {
|
||||
grid : g.append("g").attr("class", "grid"),
|
||||
area : g.append('g').attr("class", "area"),
|
||||
axis : g.append('g').attr("class", "axis"),
|
||||
point : g.append('g').attr("class", "point"),
|
||||
brush : g.append("g").attr("class", "brush"),
|
||||
tooltip: g.append("g").attr("class", "tooltip").attr('display', 'none'),
|
||||
ruler : g.append('g').attr('class', 'ruler'),
|
||||
legend : g.append('g').attr("class", "legend"),
|
||||
};
|
||||
|
||||
// SVG Paths
|
||||
const mask = panes.area .append("svg:mask") .attr("id", 'elevation-clipper-' + Math.random().toString(36).substr(2, 9)).attr('fill-opacity', 1);
|
||||
const maskRect = mask .append("svg:rect") .attr('class', 'zoom') .attr('fill', 'white'); // white = transparent
|
||||
|
||||
// Canvas Paths
|
||||
const foreignObject = panes.area .append('svg:foreignObject').attr('mask', 'url(#' + mask.attr('id') + ')');
|
||||
const canvas = foreignObject.append('xhtml:canvas') .attr('class', 'canvas-plot');
|
||||
const context = canvas.node().getContext('2d');
|
||||
|
||||
// Add tooltip
|
||||
panes.tooltip.call(Tooltip({ xCoord: 0, yCoord: 0, height: h, width : w, labels: {} }));
|
||||
|
||||
// Add the brushing
|
||||
let brush = d3.brushX().on('start.cursor end.cursor brush.cursor', () => panes.brush.select(".overlay").attr('cursor', null));
|
||||
|
||||
// Scales
|
||||
const scale = (opts) => ({ x: Scale(opts.x), y: Scale(opts.y)});
|
||||
|
||||
let utils = {
|
||||
defs,
|
||||
mask,
|
||||
canvas,
|
||||
context,
|
||||
brush,
|
||||
};
|
||||
|
||||
let chart = {
|
||||
svg,
|
||||
g,
|
||||
panes,
|
||||
utils,
|
||||
scale,
|
||||
};
|
||||
|
||||
// Resize
|
||||
chart._resize = ({
|
||||
width,
|
||||
height,
|
||||
margins = {},
|
||||
ruler,
|
||||
}) => {
|
||||
|
||||
const w = width - margins.left - margins.right;
|
||||
const h = height - margins.top - margins.bottom;
|
||||
|
||||
svg .attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`);
|
||||
g .attr("transform", "translate(" + margins.left + "," + margins.top + ")");
|
||||
|
||||
// Fix: https://github.com/Raruto/leaflet-elevation/issues/123
|
||||
// Fix: https://github.com/Raruto/leaflet-elevation/issues/232
|
||||
if (
|
||||
/Mac|iPod|iPhone|iPad/.test(navigator.platform) &&
|
||||
/AppleWebkit/i.test(navigator.userAgent) &&
|
||||
!/Chrome/.test(navigator.userAgent)
|
||||
) {
|
||||
canvas .style("transform", "translate(" + margins.left + "px," + margins.top + "px)");
|
||||
}
|
||||
|
||||
maskRect .attr("width", w).attr("height", h).attr("x", 0).attr("y", 0);
|
||||
foreignObject.attr('width', w).attr('height', h);
|
||||
canvas .attr('width', w).attr('height', h);
|
||||
|
||||
if (ruler) {
|
||||
panes.ruler.call(Ruler({ height: h, width: w }));
|
||||
}
|
||||
|
||||
panes.brush.call(brush.extent( [ [0,0], [w, h] ] ));
|
||||
panes.brush.select(".overlay").attr('cursor', null);
|
||||
|
||||
chart._width = w;
|
||||
chart._height = h;
|
||||
|
||||
chart.svg.dispatch('resize', { detail: { width: w, height: h } } );
|
||||
|
||||
};
|
||||
|
||||
chart.pane = (name) => (panes[name] || (panes[name] = g.append('g').attr("class", name)));
|
||||
chart.get = (name) => utils[name];
|
||||
|
||||
chart._resize({ width, height, margins});
|
||||
|
||||
return chart;
|
||||
};
|
||||
96
web/static/vendor/leaflet-elevation/src/components/marker.js
vendored
Normal file
96
web/static/vendor/leaflet-elevation/src/components/marker.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as D3 from './d3.js';
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
export var Marker = L.Class.extend({
|
||||
|
||||
initialize(options, control) {
|
||||
this.options = options;
|
||||
this.control = control
|
||||
|
||||
switch(this.options.marker) {
|
||||
case 'elevation-line':
|
||||
// this._container = d3.create("g").attr("class", "height-focus-group");
|
||||
break;
|
||||
case 'position-marker':
|
||||
// this._marker = L.circleMarker([0, 0], { pane: 'overlayPane', radius: 6, fillColor: '#fff', fillOpacity:1, color: '#000', weight:1, interactive: false });
|
||||
this._marker = L.marker([0, 0], { icon: this.options.markerIcon, zIndexOffset: 1000000, interactive: false });
|
||||
break;
|
||||
}
|
||||
|
||||
this._labels = {};
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
addTo(map) {
|
||||
this._map = map;
|
||||
switch(this.options.marker) {
|
||||
case 'elevation-line': this._container = d3.select(map.getPane('elevationPane')).select("svg > g").call(D3.PositionMarker({})); break;
|
||||
case 'position-marker': this._marker.addTo(map, { pane: 'overlayPane' }); break;
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update position marker ("leaflet-marker").
|
||||
*/
|
||||
update(props) {
|
||||
if (props) this._props = props;
|
||||
else props = this._props;
|
||||
|
||||
if (!props) return;
|
||||
|
||||
if (props.options) this.options = props.options;
|
||||
if (!this._map) this.addTo(props.map);
|
||||
|
||||
this._latlng = props.item.latlng;
|
||||
|
||||
switch(this.options.marker) {
|
||||
case 'elevation-line':
|
||||
if (this._container) {
|
||||
let point = this._map.latLngToLayerPoint(this._latlng);
|
||||
point = L.extend({}, props.item, this._map._rotate ? this._map.rotatedPointToMapPanePoint(point) : point);
|
||||
|
||||
let yMax = (this.control._height() / props.yCoordMax * point[this.options.yAttr]);
|
||||
|
||||
if (!isFinite(yMax) || isNaN(yMax)) yMax = 0;
|
||||
|
||||
this._container.classed("leaflet-hidden", false);
|
||||
this._container.call(D3.PositionMarker({
|
||||
theme : this.options.theme,
|
||||
xCoord: point.x,
|
||||
yCoord: point.y,
|
||||
length: point.y - yMax, // normalized Y
|
||||
labels: this._labels,
|
||||
item: point,
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case 'position-marker':
|
||||
_.removeClass(this._marker.getElement(), 'leaflet-hidden');
|
||||
this._marker.setLatLng(this._latlng);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Hides the position/height indicator marker drawn onto the map
|
||||
*/
|
||||
remove() {
|
||||
this._props = null;
|
||||
switch(this.options.marker) {
|
||||
case 'elevation-line': this._container && this._container.classed("leaflet-hidden", true); break;
|
||||
case 'position-marker': _.addClass(this._marker.getElement(), 'leaflet-hidden'); break;
|
||||
}
|
||||
},
|
||||
|
||||
getLatLng() {
|
||||
return this._latlng;
|
||||
},
|
||||
|
||||
_registerTooltip(props) {
|
||||
this._labels[props.name] = props;
|
||||
}
|
||||
|
||||
});
|
||||
43
web/static/vendor/leaflet-elevation/src/components/summary.js
vendored
Normal file
43
web/static/vendor/leaflet-elevation/src/components/summary.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
export var Summary = L.Class.extend({
|
||||
initialize(opts, control) {
|
||||
this.options = opts;
|
||||
this.control = control;
|
||||
this.labels = {};
|
||||
|
||||
let summary = this._container = _.create("div", "elevation-summary " + (opts.summary ? opts.summary + "-summary" : ''));
|
||||
_.style(summary, 'max-width', opts.width ? opts.width + 'px' : '');
|
||||
},
|
||||
|
||||
render() {
|
||||
return container => container.append(() => this._container);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this._container.innerHTML = '';
|
||||
},
|
||||
|
||||
append(className, label, value) {
|
||||
this._container.innerHTML += `<span class="${className}"><span class="summarylabel">${label}</span><span class="summaryvalue">${value}</span></span>`;
|
||||
return this;
|
||||
},
|
||||
|
||||
update() {
|
||||
Object
|
||||
.keys(this.labels)
|
||||
.sort((a, b) => this.labels[a].order - this.labels[b].order) // TODO: any performance issues?
|
||||
.forEach((i)=> {
|
||||
let value = typeof this.labels[i].value !== "function" ? this.labels[i].value : this.labels[i].value(this.control.track_info, this.labels[i].unit || '');
|
||||
this.append(i /*+ " order-" + this.labels[i].order*/, L._(this.labels[i].label), value, this.labels[i].order);
|
||||
});
|
||||
},
|
||||
|
||||
_registerSummary(data) {
|
||||
for (let i in data) {
|
||||
data[i].order = data[i].order ?? 1000;
|
||||
this.labels[i] = data[i];
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
64
web/static/vendor/leaflet-elevation/src/handlers/acceleration.js
vendored
Normal file
64
web/static/vendor/leaflet-elevation/src/handlers/acceleration.js
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
export function Acceleration() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
let opts = this.options;
|
||||
let acceleration = {};
|
||||
|
||||
acceleration.label = opts.accelerationLabel || L._(opts.imperial ? 'ft/s²' : 'm/s²');
|
||||
opts.accelerationFactor = opts.accelerationFactor || 1;
|
||||
|
||||
return {
|
||||
name: 'acceleration',
|
||||
unit: acceleration.label,
|
||||
deltaMax: this.options.accelerationDeltaMax,
|
||||
clampRange: this.options.accelerationRange,
|
||||
decimals: 2,
|
||||
pointToAttr: (_, i) => {
|
||||
let dv = (this._data[i].speed - this._data[i > 0 ? i - 1 : i].speed) * (1000 / opts.timeFactor);
|
||||
let dt = (this._data[i].time - this._data[i > 0 ? i - 1 : i].time) / 1000;
|
||||
return dt > 0 ? Math.abs((dv / dt)) * opts.accelerationFactor : NaN;
|
||||
},
|
||||
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||
scale: {
|
||||
axis : "y",
|
||||
position : "right",
|
||||
scale : { min: 0, max: +1 },
|
||||
tickPadding: 16,
|
||||
labelX : 25,
|
||||
labelY : -8,
|
||||
},
|
||||
path: {
|
||||
label : 'Acceleration',
|
||||
yAttr : 'acceleration',
|
||||
scaleX : 'distance',
|
||||
scaleY : 'acceleration',
|
||||
color : '#050402',
|
||||
strokeColor : '#000',
|
||||
strokeOpacity: "0.5",
|
||||
fillOpacity : "0.25",
|
||||
},
|
||||
tooltip: {
|
||||
chart: (item) => L._("a: ") + item.acceleration + " " + acceleration.label,
|
||||
marker: (item) => Math.round(item.acceleration) + " " + acceleration.label,
|
||||
order: 60,
|
||||
},
|
||||
summary: {
|
||||
"minacceleration" : {
|
||||
label: "Min Acceleration: ",
|
||||
value: (track, unit) => Math.round(track.acceleration_min || 0) + ' ' + unit,
|
||||
order: 60
|
||||
},
|
||||
"maxacceleration" : {
|
||||
label: "Max Acceleration: ",
|
||||
value: (track, unit) => Math.round(track.acceleration_max || 0) + ' ' + unit,
|
||||
order: 61
|
||||
},
|
||||
"avgacceleration": {
|
||||
label: "Avg Acceleration: ",
|
||||
value: (track, unit) => Math.round(track.acceleration_avg || 0) + ' ' + unit,
|
||||
order: 62
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
78
web/static/vendor/leaflet-elevation/src/handlers/altitude.js
vendored
Normal file
78
web/static/vendor/leaflet-elevation/src/handlers/altitude.js
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
export function Altitude() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
let opts = this.options;
|
||||
let altitude = {};
|
||||
|
||||
let theme = opts.theme.split(' ')[0].replace('-theme', '');
|
||||
let color = _.Colors[theme] || {};
|
||||
|
||||
opts.altitudeFactor = opts.imperial ? this.__footFactor : (opts.altitudeFactor || 1); // 1 m = (1 m)
|
||||
altitude.label = opts.imperial ? "ft" : opts.yLabel;
|
||||
|
||||
return {
|
||||
name: 'altitude',
|
||||
required: this.options.slope,
|
||||
meta: 'z',
|
||||
unit: altitude.label,
|
||||
statsName: 'elevation',
|
||||
deltaMax: this.options.altitudeDeltaMax,
|
||||
clampRange: this.options.altitudeRange,
|
||||
// init: ({point}) => {
|
||||
// // "alt" property is generated inside "leaflet"
|
||||
// if ("alt" in point) point.meta.ele = point.alt;
|
||||
// },
|
||||
pointToAttr: (point, i) => {
|
||||
if ("alt" in point) point.meta.ele = point.alt; // "alt" property is generated inside "leaflet"
|
||||
return this._data[i].z *= opts.altitudeFactor;
|
||||
},
|
||||
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||
grid: {
|
||||
axis : "y",
|
||||
position : "left",
|
||||
scale : "y" // this._chart._y,
|
||||
},
|
||||
scale: {
|
||||
axis : "y",
|
||||
position: "left",
|
||||
scale : "y", // this._chart._y,
|
||||
labelX : -3,
|
||||
labelY : -8,
|
||||
},
|
||||
path: {
|
||||
label : 'Altitude',
|
||||
scaleX : 'distance',
|
||||
scaleY : 'altitude',
|
||||
className : 'area',
|
||||
color : color.area || theme,
|
||||
strokeColor : opts.detached ? color.stroke : '#000',
|
||||
strokeOpacity: "1",
|
||||
fillOpacity : opts.detached ? (color.alpha || '0.8') : 1,
|
||||
preferCanvas : opts.preferCanvas,
|
||||
},
|
||||
tooltip: {
|
||||
name: 'y',
|
||||
chart: (item) => L._("y: ") + _.round(item[opts.yAttr], opts.decimalsY) + " " + altitude.label,
|
||||
marker: (item) => _.round(item[opts.yAttr], opts.decimalsY) + " " + altitude.label,
|
||||
order: 10,
|
||||
},
|
||||
summary: {
|
||||
"minele" : {
|
||||
label: "Min Elevation: ",
|
||||
value: (track, unit) => (track.elevation_min || 0).toFixed(2) + ' ' + unit,
|
||||
order: 30,
|
||||
},
|
||||
"maxele" : {
|
||||
label: "Max Elevation: ",
|
||||
value: (track, unit) => (track.elevation_max || 0).toFixed(2) + ' ' + unit,
|
||||
order: 31,
|
||||
},
|
||||
"avgele" : {
|
||||
label: "Avg Elevation: ",
|
||||
value: (track, unit) => (track.elevation_avg || 0).toFixed(2) + ' ' + unit,
|
||||
order: 32,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
54
web/static/vendor/leaflet-elevation/src/handlers/cadence.js
vendored
Normal file
54
web/static/vendor/leaflet-elevation/src/handlers/cadence.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
export function Cadence() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
return {
|
||||
name: 'cadence', // <-- Your custom option name (eg. "cadence: true")
|
||||
unit: 'rpm',
|
||||
meta: 'cad', // <-- point.meta.cad
|
||||
coordinateProperties: ["cads", "cadences", "cad", "cadence"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
|
||||
pointToAttr: (point, i) => (point.cad ?? point.meta.cad ?? point.prev('cadence')) || 0,
|
||||
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||
scale: {
|
||||
axis : "y",
|
||||
position : "right",
|
||||
scale : { min: -1, max: +1 },
|
||||
tickPadding: 16,
|
||||
labelX : 25,
|
||||
labelY : -8,
|
||||
},
|
||||
path: {
|
||||
label : 'RPM',
|
||||
yAttr : 'cadence',
|
||||
scaleX : 'distance',
|
||||
scaleY : 'cadence',
|
||||
color : '#FFF',
|
||||
strokeColor : 'blue',
|
||||
strokeOpacity: "0.85",
|
||||
fillOpacity : "0.1",
|
||||
},
|
||||
tooltip: {
|
||||
name: 'cadence',
|
||||
chart: (item) => L._("cad: ") + item.cadence + " " + 'rpm',
|
||||
marker: (item) => Math.round(item.cadence) + " " + 'rpm',
|
||||
order: 1
|
||||
},
|
||||
summary: {
|
||||
"minrpm": {
|
||||
label: "Min RPM: ",
|
||||
value: (track, unit) => Math.round(track.cadence_min || 0) + ' ' + unit,
|
||||
// order: 30
|
||||
},
|
||||
"maxrpm": {
|
||||
label: "Max RPM: ",
|
||||
value: (track, unit) => Math.round(track.cadence_max || 0) + ' ' + unit,
|
||||
// order: 30
|
||||
},
|
||||
"avgrpm": {
|
||||
label: "Avg RPM: ",
|
||||
value: (track, unit) => Math.round(track.cadence_avg || 0) + ' ' + unit,
|
||||
// order: 20
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
46
web/static/vendor/leaflet-elevation/src/handlers/distance.js
vendored
Normal file
46
web/static/vendor/leaflet-elevation/src/handlers/distance.js
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
export function Distance() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
let opts = this.options;
|
||||
let distance = {};
|
||||
|
||||
opts.distanceFactor = opts.imperial ? this.__mileFactor : (opts.distanceFactor || 1); // 1 km = (1000 m)
|
||||
distance.label = opts.imperial ? "mi" : opts.xLabel;
|
||||
|
||||
return {
|
||||
name: 'distance',
|
||||
required: true,
|
||||
attr: 'dist',
|
||||
unit: distance.label,
|
||||
decimals: 5,
|
||||
pointToAttr: (_, i) => (i > 0 ? this._data[i - 1].dist : 0) + (this._data[i].latlng.distanceTo(this._data[i > 0 ? i - 1 : i].latlng) * opts.distanceFactor) / 1000, // convert back km to meters
|
||||
// stats: { total: _.iSum },
|
||||
onPointAdded: (distance, i) => this.track_info.distance = distance,
|
||||
scale: opts.distance && {
|
||||
axis : "x",
|
||||
position: "bottom",
|
||||
scale : "x", // this._chart._x,
|
||||
labelY : 25,
|
||||
labelX : () => this._width() + 6,
|
||||
ticks : () => _.clamp(this._chart._xTicks() / 2, [4, +Infinity]),
|
||||
},
|
||||
grid: opts.distance && {
|
||||
axis : "x",
|
||||
position : "bottom",
|
||||
scale : "x" // this._chart._x,
|
||||
},
|
||||
tooltip: opts.distance && {
|
||||
name: 'x',
|
||||
chart: (item) => L._("x: ") + _.round(item[opts.xAttr], opts.decimalsX) + " " + distance.label,
|
||||
order: 20
|
||||
},
|
||||
summary: opts.distance && {
|
||||
"totlen" : {
|
||||
label: "Total Length: ",
|
||||
value: (track) => (track.distance || 0).toFixed(2) + ' ' + distance.label,
|
||||
order: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
53
web/static/vendor/leaflet-elevation/src/handlers/heart.js
vendored
Normal file
53
web/static/vendor/leaflet-elevation/src/handlers/heart.js
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
export function Heart() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
return {
|
||||
name: 'heart', // <-- Your custom option name (eg. "heart: true")
|
||||
unit: 'bpm',
|
||||
meta: 'hr', // <-- point.meta.hr
|
||||
coordinateProperties: ["heart", "heartRates", "heartRate"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
|
||||
pointToAttr: (point, i) => (point.hr ?? point.meta.hr ?? point.prev('heart')) || 0,
|
||||
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||
scale: {
|
||||
axis : "y",
|
||||
position : "left",
|
||||
scale : { min: -1, max: +1 },
|
||||
tickPadding: 25,
|
||||
labelX : -30,
|
||||
labelY : -8,
|
||||
},
|
||||
path: {
|
||||
label : 'ECG',
|
||||
yAttr : 'heart',
|
||||
scaleX : 'distance',
|
||||
scaleY : 'heart',
|
||||
color : 'white',
|
||||
strokeColor : 'red',
|
||||
strokeOpacity: "0.85",
|
||||
fillOpacity : "0.1",
|
||||
},
|
||||
tooltip: {
|
||||
chart: (item) => L._("hr: ") + item.heart + " " + 'bpm',
|
||||
marker: (item) => Math.round(item.heart) + " " + 'bpm',
|
||||
order: 1
|
||||
},
|
||||
summary: {
|
||||
"minbpm": {
|
||||
label: "Min BPM: ",
|
||||
value: (track, unit) => Math.round(track.heart_min || 0) + ' ' + unit,
|
||||
// order: 30
|
||||
},
|
||||
"maxbpm": {
|
||||
label: "Max BPM: ",
|
||||
value: (track, unit) => Math.round(track.heart_max || 0) + ' ' + unit,
|
||||
// order: 30
|
||||
},
|
||||
"avgbpm": {
|
||||
label: "Avg BPM: ",
|
||||
value: (track, unit) => Math.round(track.heart_avg || 0) + ' ' + unit,
|
||||
// order: 20
|
||||
},
|
||||
}
|
||||
};
|
||||
};
|
||||
45
web/static/vendor/leaflet-elevation/src/handlers/labels.js
vendored
Normal file
45
web/static/vendor/leaflet-elevation/src/handlers/labels.js
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @see https://github.com/Raruto/leaflet-elevation/issues/211
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* L.control.Elevation({ handlers: [ 'Labels' ], labelsRotation: 25, labelsAlign: 'start' })
|
||||
* ```
|
||||
*/
|
||||
export function Labels() {
|
||||
|
||||
this.on('elechart_updated', function(e) {
|
||||
|
||||
const pointG = this._chart._chart.pane('point');
|
||||
|
||||
const textRotation = this.options.labelsRotation ?? -90;
|
||||
const textAnchor = this.options.labelsAlign;
|
||||
|
||||
if (90 == Math.abs(textRotation)) {
|
||||
|
||||
pointG.selectAll('text')
|
||||
.attr('dy', '4px')
|
||||
.attr("dx", (d, i, el) => Math.sign(textRotation) * (this._height() - d3.select(el[i].parentElement).datum().y - 8) + 'px')
|
||||
.attr('text-anchor', textRotation > 0 ? 'end' : 'start')
|
||||
.attr('transform', 'rotate(' + textRotation + ')')
|
||||
|
||||
pointG.selectAll('circle')
|
||||
.attr('r', '2.5')
|
||||
.attr('fill', '#fff')
|
||||
.attr("stroke", '#000')
|
||||
.attr("stroke-width", '1.1');
|
||||
|
||||
} else if (!isNaN(textRotation)) {
|
||||
|
||||
pointG.selectAll('text')
|
||||
.attr("dx", "4px")
|
||||
.attr("dy", "-9px")
|
||||
.attr('text-anchor', textAnchor ?? (0 == textRotation ? 'start' : 'middle'))
|
||||
.attr('transform', 'rotate('+ -textRotation +')')
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return { };
|
||||
}
|
||||
122
web/static/vendor/leaflet-elevation/src/handlers/lineargradient.js
vendored
Normal file
122
web/static/vendor/leaflet-elevation/src/handlers/lineargradient.js
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @see https://github.com/Raruto/leaflet-elevation/issues/251
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* L.control.Elevation({
|
||||
* altitude: true,
|
||||
* distance: true,
|
||||
* handlers: [ 'Altitude', 'Distance', 'LinearGradient', ],
|
||||
* linearGradient: {
|
||||
* attr: 'z',
|
||||
* path: 'altitude',
|
||||
* range: { 0.0: '#008800', 0.5: '#ffff00', 1.0: '#ff0000' },
|
||||
* min: 'elevation_min',
|
||||
* max: 'elevation_max',
|
||||
* },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function LinearGradient() {
|
||||
|
||||
if (!this.options.linearGradient) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
/**
|
||||
* Initialize gradient color palette.
|
||||
*/
|
||||
const get_palette = function ({range, min, max, depth = 256}) {
|
||||
const canvas = document.createElement('canvas'),
|
||||
ctx = canvas.getContext('2d'),
|
||||
gradient = ctx.createLinearGradient(0, 0, 0, depth);
|
||||
|
||||
canvas.width = 1;
|
||||
canvas.height = depth;
|
||||
|
||||
for (let i in range) {
|
||||
gradient.addColorStop(i, range[i]);
|
||||
}
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, 1, depth);
|
||||
|
||||
const { data } = ctx.getImageData(0, 0, 1, depth);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Gets the RGB values of a given z value of the current palette.
|
||||
*
|
||||
* @param {number} value - Value to get the color for, should be between min and max.
|
||||
* @returns {string} The RGB values as `rgb(r, g, b)` string
|
||||
*/
|
||||
getRGBColor(value) {
|
||||
const idx = Math.floor(Math.min(Math.max((value - min) / (max - min), 0), 0.999) * depth) * 4;
|
||||
return 'rgb(' + [data[idx], data[idx + 1], data[idx + 2]].join(',') + ')';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const { preferCanvas } = this.options;
|
||||
const { attr, path: path_name, range, min, max } = L.extend({
|
||||
attr: 'z',
|
||||
path: 'altitude',
|
||||
range: { 0.0: '#008800', 0.5: '#ffff00', 1.0: '#ff0000' },
|
||||
min: 'elevation_min',
|
||||
max: 'elevation_max',
|
||||
}, (true === this.options.linearGradient) ? {} : this.options.linearGradient);
|
||||
|
||||
const gradient_id = path_name + '-gradient-' + _.randomId();
|
||||
const legend_id = 'legend-' + gradient_id;
|
||||
|
||||
// Charte profile gradient
|
||||
this.on('elechart_axis', () => {
|
||||
if (!this._data.length) return;
|
||||
|
||||
const chart = this._chart;
|
||||
const path = chart._paths[path_name];
|
||||
const { defs } = chart._chart.utils;
|
||||
|
||||
const palette = get_palette({
|
||||
min: isFinite(this.track_info[min]) ? this.track_info[min] : 0,
|
||||
max: isFinite(this.track_info[max]) ? this.track_info[max] : 1,
|
||||
range,
|
||||
});
|
||||
|
||||
let gradient;
|
||||
|
||||
if (preferCanvas) {
|
||||
/** ref: `path.__fillStyle` within L.Control.Elevation.Utils::drawCanvas(ctx, path) */
|
||||
path.__fillStyle = gradient = chart._context.createLinearGradient(0, 0, chart._width(), 0);
|
||||
} else {
|
||||
defs.select('#' + gradient_id).remove();
|
||||
gradient = defs.append('svg:linearGradient').attr('id', gradient_id);
|
||||
gradient.addColorStop = function(offset, color) { gradient.append('svg:stop').attr('offset', offset).attr('stop-color', color) };
|
||||
path.attr('fill', 'url(#' + gradient_id + ')').classed('area', false);
|
||||
}
|
||||
|
||||
// Generate gradient for each segment picking colors from palette
|
||||
for (let i = 0, data = this._data; i < data.length; i++) {
|
||||
gradient.addColorStop((i) / data.length, palette.getRGBColor(data[i][attr]));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Legend item gradient
|
||||
this.on('elechart_updated', () => {
|
||||
const chart = this._chart;
|
||||
const { defs } = chart._chart.utils;
|
||||
defs.select('#' + legend_id).remove();
|
||||
const legendGradient = defs.append('svg:linearGradient').attr('id', legend_id);
|
||||
Object.keys(range).sort().forEach(i => legendGradient.append('svg:stop').attr('offset', i).attr('stop-color', range[i]));
|
||||
|
||||
chart._container
|
||||
.select('.legend-' + path_name + ' > rect')
|
||||
.attr('fill', 'url(#' + legend_id + ')')
|
||||
.classed('area', false);
|
||||
});
|
||||
|
||||
return { };
|
||||
}
|
||||
65
web/static/vendor/leaflet-elevation/src/handlers/pace.js
vendored
Normal file
65
web/static/vendor/leaflet-elevation/src/handlers/pace.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
export function Pace() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
let opts = this.options;
|
||||
let pace = {};
|
||||
|
||||
pace.label = opts.paceLabel || L._(opts.imperial ? 'min/mi' : 'min/km');
|
||||
opts.paceFactor = opts.paceFactor || 60; // 1 min = 60 sec
|
||||
|
||||
return {
|
||||
name: 'pace',
|
||||
unit: pace.label,
|
||||
deltaMax: this.options.paceDeltaMax,
|
||||
clampRange: this.options.paceRange,
|
||||
decimals: 2,
|
||||
pointToAttr: (_, i) => {
|
||||
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
|
||||
let dt = this._data[i].time - this._data[ i > 0 ? i - 1 : i].time;
|
||||
return dx > 0 ? Math.abs((dt / dx) / opts.paceFactor) : NaN;
|
||||
},
|
||||
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||
scale : (this.options.pace && this.options.pace != "summary") && {
|
||||
axis : "y",
|
||||
position : "right",
|
||||
scale : { min : 0, max : +1 },
|
||||
tickPadding: 16,
|
||||
labelX : 25,
|
||||
labelY : -8,
|
||||
},
|
||||
path: (this.options.pace && this.options.pace != "summary") && {
|
||||
// name : 'pace',
|
||||
label : 'Pace',
|
||||
yAttr : "pace",
|
||||
scaleX : 'distance',
|
||||
scaleY : 'pace',
|
||||
color : '#03ffff',
|
||||
strokeColor : '#000',
|
||||
strokeOpacity: "0.5",
|
||||
fillOpacity : "0.25",
|
||||
},
|
||||
tooltip: (this.options.pace) && {
|
||||
chart: (item) => L._('pace: ') + item.pace + " " + pace.label,
|
||||
marker: (item) => Math.round(item.pace) + " " + pace.label,
|
||||
order: 50,
|
||||
},
|
||||
summary: (this.options.pace) && {
|
||||
"minpace" : {
|
||||
label: "Min Pace: ",
|
||||
value: (track, unit) => Math.round(track.pace_min || 0) + ' ' + unit,
|
||||
order: 51
|
||||
},
|
||||
"maxpace" : {
|
||||
label: "Max Pace: ",
|
||||
value: (track, unit) => Math.round(track.pace_max || 0) + ' ' + unit,
|
||||
order: 51
|
||||
},
|
||||
"avgpace": {
|
||||
label: "Avg Pace: ",
|
||||
value: (track, unit) => Math.round(track.pace_avg || 0) + ' ' + unit,
|
||||
order: 52
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
109
web/static/vendor/leaflet-elevation/src/handlers/runner.js
vendored
Normal file
109
web/static/vendor/leaflet-elevation/src/handlers/runner.js
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @see https://github.com/Igor-Vladyka/leaflet.motion
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* L.control.Elevation({ handlers: [ 'Runner' ], runnerOptions: { polyline: {..}, motion: {..}, marker {..} } })
|
||||
* ```
|
||||
*/
|
||||
export async function Runner() {
|
||||
|
||||
await this.import(this.__LMOTION || 'https://unpkg.com/leaflet.motion@0.3.2/dist/leaflet.motion.min.js', typeof L.Motion !== 'object')
|
||||
|
||||
let { runnerOptions } = this.options;
|
||||
|
||||
runnerOptions = L.extend(
|
||||
{ polyline: {}, motion: {}, marker: undefined },
|
||||
'object' === typeof runnerOptions ? runnerOptions : {}
|
||||
);
|
||||
|
||||
// Custom tooltips
|
||||
this._registerTooltip({
|
||||
name: 'here',
|
||||
marker: (item) => L._("You are here: "),
|
||||
order: 1,
|
||||
});
|
||||
|
||||
this._registerTooltip({
|
||||
name: 'distance',
|
||||
marker: (item) => Math.round(item.dist) + " " + this.options.xLabel,
|
||||
order: 2,
|
||||
});
|
||||
|
||||
this.addCheckpoint = function (checkpoint) {
|
||||
return this._registerCheckPoint({ // <-- NB these are private functions use them at your own risk!
|
||||
latlng: this._findItemForX(this._x(checkpoint.dist)).latlng,
|
||||
label: checkpoint.label || ''
|
||||
});
|
||||
}
|
||||
|
||||
this.addRunner = function (runner) {
|
||||
let x = this._x(runner.dist);
|
||||
let y = this._y(this._findItemForX(x).z)
|
||||
let g = d3.select(this._container)
|
||||
.select('svg > g')
|
||||
.append("svg:circle")
|
||||
.attr("class", "runner " + this.options.theme + " height-focus circle-lower")
|
||||
.attr("r", 6)
|
||||
.attr("cx", x)
|
||||
.attr("cy", y);
|
||||
return g;
|
||||
}
|
||||
|
||||
this.setPositionFromLatLng = function (latlng) {
|
||||
this._onMouseMoveLayer({ latlng: latlng }); // Update map and chart "markers" from latlng
|
||||
};
|
||||
|
||||
this.tick = function (runner, dist = 0, inc = 0.1) {
|
||||
dist = (dist <= this.track_info.distance - inc) ? dist + inc : 0;
|
||||
this.updateRunnerPos(runner, dist);
|
||||
setTimeout(() => this.tick(runner, dist), 150);
|
||||
};
|
||||
|
||||
this.updateRunnerPos = function (runner, pos) {
|
||||
let curr, x;
|
||||
|
||||
if (pos instanceof L.LatLng) {
|
||||
curr = this._findItemForLatLng(pos);
|
||||
x = this._x(curr.dist);
|
||||
} else {
|
||||
x = this._x(pos);
|
||||
curr = this._findItemForX(x);
|
||||
}
|
||||
|
||||
runner
|
||||
.attr("cx", x)
|
||||
.attr("cy", this._y(curr.z));
|
||||
this.setPositionFromLatLng(curr.latlng);
|
||||
};
|
||||
|
||||
this.animate = function (layer, speed = 1500) {
|
||||
|
||||
if (this._runner) {
|
||||
this._runner.remove();
|
||||
}
|
||||
|
||||
layer.setStyle({ opacity: 0.5 });
|
||||
|
||||
const geo = L.geoJson(layer.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1)) });
|
||||
this._runner = L.motion.polyline(
|
||||
geo.toGeoJSON().features[0].geometry.coordinates,
|
||||
L.extend({}, { color: 'red', pane: 'elevationPane', attribution: '' }, runnerOptions.polyline),
|
||||
L.extend({}, { auto: true, speed: speed, }, runnerOptions.motion),
|
||||
runnerOptions.marker || undefined
|
||||
);
|
||||
|
||||
// Override default function behavior: `L.Motion.Polyline::_drawMarker()`
|
||||
this._runner._drawMarker = new Proxy(this._runner._drawMarker, {
|
||||
apply: (target, thisArg, argArray) => {
|
||||
thisArg._runner = thisArg._runner || this.addRunner({ dist: 0 });
|
||||
this.updateRunnerPos(thisArg._runner, argArray[0]);
|
||||
return target.apply(thisArg, argArray);
|
||||
}
|
||||
});
|
||||
|
||||
this._runner.addTo(this._map);
|
||||
};
|
||||
|
||||
return {};
|
||||
}
|
||||
79
web/static/vendor/leaflet-elevation/src/handlers/slope.js
vendored
Normal file
79
web/static/vendor/leaflet-elevation/src/handlers/slope.js
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
export function Slope() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
let opts = this.options;
|
||||
let slope = {};
|
||||
|
||||
slope.label = opts.slopeLabel || '%';
|
||||
|
||||
return {
|
||||
name: 'slope',
|
||||
meta: 'slope',
|
||||
unit: slope.label,
|
||||
deltaMax: this.options.slopedDeltaMax,
|
||||
clampRange: this.options.slopeRange,
|
||||
decimals: 2,
|
||||
pointToAttr: (_, i) => { // slope in % = ( dy / dx ) * 100;
|
||||
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
|
||||
let dy = this._data[i][this.options.yAttr] - this._data[i > 0 ? i - 1 : i][this.options.yAttr];
|
||||
return dx !== 0 ? (dy / dx) * 100 : NaN;
|
||||
},
|
||||
onPointAdded: (_, i) => {
|
||||
let dz = this._data[i][this.options.yAttr] - this._data[i > 0 ? i - 1 : i][this.options.yAttr];
|
||||
if (dz > 0) this.track_info.ascent = (this.track_info.ascent || 0) + dz; // Total Ascent
|
||||
else if (dz < 0) this.track_info.descent = (this.track_info.descent || 0) - dz; // Total Descent
|
||||
},
|
||||
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg, },
|
||||
scale: {
|
||||
axis : "y",
|
||||
position : "right",
|
||||
scale : { min: -1, max: +1 },
|
||||
tickPadding: 16,
|
||||
labelX : 25,
|
||||
labelY : -8,
|
||||
},
|
||||
path: {
|
||||
label : 'Slope',
|
||||
yAttr : 'slope',
|
||||
scaleX : 'distance',
|
||||
scaleY : 'slope',
|
||||
color : '#F00',
|
||||
strokeColor : '#000',
|
||||
strokeOpacity: "0.5",
|
||||
fillOpacity : "0.25",
|
||||
},
|
||||
tooltip: {
|
||||
chart: (item) => L._("m: ") + item.slope + slope.label,
|
||||
marker: (item) => Math.round(item.slope) + slope.label,
|
||||
order: 40,
|
||||
},
|
||||
summary: {
|
||||
"minslope": {
|
||||
label: "Min Slope: ",
|
||||
value: (track, unit) => Math.round(track.slope_min || 0) + ' ' + unit,
|
||||
order: 40
|
||||
},
|
||||
"maxslope": {
|
||||
label: "Max Slope: ",
|
||||
value: (track, unit) => Math.round(track.slope_max || 0) + ' ' + unit,
|
||||
order: 41
|
||||
},
|
||||
"avgslope": {
|
||||
label: "Avg Slope: ",
|
||||
value: (track, unit) => Math.round(track.slope_avg || 0) + ' ' + unit,
|
||||
order: 42
|
||||
},
|
||||
"ascent" : {
|
||||
label: "Total Ascent: ",
|
||||
value: (track, unit) => Math.round(track.ascent || 0) + ' ' + (this.options.imperial ? 'ft' : 'm'),
|
||||
order: 43
|
||||
},
|
||||
"descent" : {
|
||||
label: "Total Descent: ",
|
||||
value: (track, unit) => Math.round(track.descent || 0) + ' ' + (this.options.imperial ? 'ft' : 'm'),
|
||||
order: 45
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
66
web/static/vendor/leaflet-elevation/src/handlers/speed.js
vendored
Normal file
66
web/static/vendor/leaflet-elevation/src/handlers/speed.js
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
export function Speed() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
let opts = this.options;
|
||||
let speed = {};
|
||||
|
||||
speed.label = opts.speedLabel || L._(opts.imperial ? 'mph' : 'km/h');
|
||||
opts.speedFactor = opts.speedFactor || 1;
|
||||
|
||||
return {
|
||||
name: 'speed',
|
||||
required: (this.options.acceleration),
|
||||
unit: speed.label,
|
||||
deltaMax: this.options.speedDeltaMax,
|
||||
clampRange: this.options.speedRange,
|
||||
decimals: 2,
|
||||
pointToAttr: (_, i) => {
|
||||
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
|
||||
let dt = this._data[i].time - this._data[ i > 0 ? i - 1 : i].time;
|
||||
return dt > 0 ? Math.abs((dx / dt) * opts.timeFactor) * opts.speedFactor : NaN;
|
||||
},
|
||||
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||
scale : (this.options.speed && this.options.speed != "summary") && {
|
||||
axis : "y",
|
||||
position : "right",
|
||||
scale : { min : 0, max : +1 },
|
||||
tickPadding: 16,
|
||||
labelX : 25,
|
||||
labelY : -8,
|
||||
},
|
||||
path: (this.options.speed && this.options.speed != "summary") && {
|
||||
// name : 'speed',
|
||||
label : 'Speed',
|
||||
yAttr : "speed",
|
||||
scaleX : 'distance',
|
||||
scaleY : 'speed',
|
||||
color : '#03ffff',
|
||||
strokeColor : '#000',
|
||||
strokeOpacity: "0.5",
|
||||
fillOpacity : "0.25",
|
||||
},
|
||||
tooltip: (this.options.speed) && {
|
||||
chart: (item) => L._('v: ') + item.speed + " " + speed.label,
|
||||
marker: (item) => Math.round(item.speed) + " " + speed.label,
|
||||
order: 50,
|
||||
},
|
||||
summary: (this.options.speed) && {
|
||||
"minspeed" : {
|
||||
label: "Min Speed: ",
|
||||
value: (track, unit) => Math.round(track.speed_min || 0) + ' ' + unit,
|
||||
order: 51
|
||||
},
|
||||
"maxspeed" : {
|
||||
label: "Max Speed: ",
|
||||
value: (track, unit) => Math.round(track.speed_max || 0) + ' ' + unit,
|
||||
order: 51
|
||||
},
|
||||
"avgspeed": {
|
||||
label: "Avg Speed: ",
|
||||
value: (track, unit) => Math.round(track.speed_avg || 0) + ' ' + unit,
|
||||
order: 52
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
62
web/static/vendor/leaflet-elevation/src/handlers/temperature.js
vendored
Normal file
62
web/static/vendor/leaflet-elevation/src/handlers/temperature.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
export function Temperature() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
let temperature = {};
|
||||
let opts = this.options;
|
||||
temperature.label = opts.label || L._(opts.imperial ? '°F' : '°C');
|
||||
|
||||
// Fahrenheit = (Celsius * 9/5) + 32
|
||||
opts.temperatureFactor1 = opts.temperatureFactor1 ?? (opts.imperial ? 1.8 : 1);
|
||||
opts.temperatureFactor2 = opts.temperatureFactor2 ?? (opts.imperial ? 32 : 0);
|
||||
|
||||
return {
|
||||
name: 'temperature', // <-- Your custom option name (eg. "temperature: true")
|
||||
unit: temperature.label,
|
||||
meta: 'atemps', // <-- point.meta.atemps
|
||||
coordinateProperties: ["atemps"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
|
||||
deltaMax: this.options.temperatureDeltaMax,
|
||||
clampRange: this.options.temperatureRange,
|
||||
decimals: 2,
|
||||
pointToAttr: (point, i) => (point.meta.atemps ?? point.meta.atemps ?? point.prev('temperature')) * opts.temperatureFactor1 + opts.temperatureFactor2,
|
||||
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||
scale: {
|
||||
axis : "y",
|
||||
position : "right",
|
||||
scale : { min: -1, max: +1 },
|
||||
tickPadding: 16,
|
||||
labelX : +18,
|
||||
labelY : -8,
|
||||
},
|
||||
path: {
|
||||
label : temperature.label,
|
||||
yAttr : 'temperature',
|
||||
scaleX : 'distance',
|
||||
scaleY : 'temperature',
|
||||
color : 'transparent',
|
||||
strokeColor : '#000',
|
||||
strokeOpacity: "0.85",
|
||||
// fillOpacity : "0.1",
|
||||
},
|
||||
tooltip: {
|
||||
name: 'temperature',
|
||||
chart: (item) => L._("Temp: ") + Math.round(item.temperature).toLocaleString() + " " + temperature.label,
|
||||
marker: (item) => Math.round(item.temperature).toLocaleString() + " " + temperature.label,
|
||||
order: 1
|
||||
},
|
||||
summary: {
|
||||
"mintemp": {
|
||||
label: "Min Temp: ",
|
||||
value: (track, unit) => Math.round(track.temperature_min || 0) + ' ' + unit,
|
||||
},
|
||||
"maxtemp": {
|
||||
label: "Max Temp: ",
|
||||
value: (track, unit) => Math.round(track.temperature_max || 0) + ' ' + unit,
|
||||
},
|
||||
"avgtemp": {
|
||||
label: "Avg Temp: ",
|
||||
value: (track, unit) => Math.round(track.temperature_avg || 0) + ' ' + unit,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
103
web/static/vendor/leaflet-elevation/src/handlers/time.js
vendored
Normal file
103
web/static/vendor/leaflet-elevation/src/handlers/time.js
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
export function Time() {
|
||||
|
||||
const _ = L.Control.Elevation.Utils;
|
||||
|
||||
let opts = this.options;
|
||||
let time = {};
|
||||
|
||||
time.label = opts.timeLabel || 't';
|
||||
opts.timeFactor = opts.timeFactor || 3600;
|
||||
|
||||
/**
|
||||
* Common AVG speeds:
|
||||
* ----------------------
|
||||
* slow walk = 1.8 km/h
|
||||
* walking = 3.6 km/h <-- default: 3.6
|
||||
* running = 10.8 km/h
|
||||
* cycling = 18 km/h
|
||||
* driving = 72 km/h
|
||||
* ----------------------
|
||||
*/
|
||||
this._timeAVGSpeed = (opts.timeAVGSpeed || 3.6) * (opts.speedFactor || 1);
|
||||
|
||||
if (!opts.timeFormat) {
|
||||
opts.timeFormat = (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
|
||||
} else if (opts.timeFormat == 'time') {
|
||||
opts.timeFormat = (time) => (new Date(time)).toLocaleTimeString();
|
||||
} else if (opts.timeFormat == 'date') {
|
||||
opts.timeFormat = (time) => (new Date(time)).toLocaleDateString();
|
||||
}
|
||||
|
||||
opts.xTimeFormat = opts.xTimeFormat || ((t) => _.formatTime(t).split("'")[0]);
|
||||
|
||||
return {
|
||||
name: 'time',
|
||||
required: (this.options.speed || this.options.acceleration || this.options.timestamps),
|
||||
coordinateProperties: ["coordTimes", "times", "time"],
|
||||
coordPropsToMeta: _.parseDate,
|
||||
pointToAttr: function(point, i) {
|
||||
// Add missing timestamps (see: options.timeAVGSpeed)
|
||||
if (!point.meta || !point.meta.time) {
|
||||
point.meta = point.meta || {};
|
||||
if (i > 0) {
|
||||
let dx = (this._data[i].dist - this._data[i - 1].dist);
|
||||
let t0 = this._data[i - 1].time.getTime();
|
||||
point.meta.time = new Date(t0 + ( dx / this._timeAVGSpeed) * this.options.timeFactor * 1000);
|
||||
} else {
|
||||
point.meta.time = new Date(Date.now())
|
||||
}
|
||||
}
|
||||
// Handle timezone offset
|
||||
let time = (point.meta.time.getTime() - point.meta.time.getTimezoneOffset() * 60 * 1000 !== 0) ? point.meta.time : 0;
|
||||
// Update duration
|
||||
this._data[i].duration = i > 0 ? (this._data[i - 1].duration || 0) + Math.abs(time - this._data[i - 1].time) : 0;
|
||||
return time;
|
||||
},
|
||||
onPointAdded: (_, i) => this.track_info.time = this._data[i].duration,
|
||||
scale: (opts.time && opts.time != "summary" && !L.Browser.mobile) && {
|
||||
axis : "x",
|
||||
position : "top",
|
||||
scale : {
|
||||
attr : "duration",
|
||||
min : 0,
|
||||
},
|
||||
label : time.label,
|
||||
labelY : -10,
|
||||
labelX : () => this._width(),
|
||||
name : "time",
|
||||
ticks : () => _.clamp(this._chart._xTicks() / 2, [4, +Infinity]),
|
||||
tickFormat : (d) => (d == 0 ? '' : opts.xTimeFormat(d)),
|
||||
onAxisMount: axis => {
|
||||
axis.select(".domain")
|
||||
.remove();
|
||||
axis.selectAll("text")
|
||||
.attr('opacity', 0.65)
|
||||
.style('font-family', 'Monospace')
|
||||
.style('font-size', '110%');
|
||||
axis.selectAll(".tick line")
|
||||
.attr('y2', this._height())
|
||||
.attr('stroke-dasharray', 2)
|
||||
.attr('opacity', 0.75);
|
||||
}
|
||||
},
|
||||
tooltips: [
|
||||
(this.options.time) && {
|
||||
name: 'time',
|
||||
chart: (item) => L._("T: ") + _.formatTime(item.duration || 0),
|
||||
order: 20
|
||||
},
|
||||
(this.options.timestamps) && {
|
||||
name: 'date',
|
||||
chart: (item) => L._("t: ") + this.options.timeFormat(item.time),
|
||||
order: 21,
|
||||
}
|
||||
],
|
||||
summary: (this.options.time) && {
|
||||
"tottime" : {
|
||||
label: "Total Time: ",
|
||||
value: (track) => _.formatTime(track.time || 0),
|
||||
order: 20
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user