457 lines
11 KiB
JavaScript
457 lines
11 KiB
JavaScript
/*
|
|
(c) 2017, iosphere GmbH
|
|
Leaflet.hotline, a Leaflet plugin for drawing gradients along polylines.
|
|
https://github.com/iosphere/Leaflet.hotline/
|
|
*/
|
|
|
|
(function (root, plugin) {
|
|
/**
|
|
* UMD wrapper.
|
|
* When used directly in the Browser it expects Leaflet to be globally
|
|
* available as `L`. The plugin then adds itself to Leaflet.
|
|
* When used as a CommonJS module (e.g. with browserify) only the plugin
|
|
* factory gets exported, so one hast to call the factory manually and pass
|
|
* Leaflet as the only parameter.
|
|
* @see {@link https://github.com/umdjs/umd}
|
|
*/
|
|
if (typeof define === 'function' && define.amd) {
|
|
define(['leaflet'], plugin);
|
|
} else if (typeof exports === 'object') {
|
|
module.exports = plugin;
|
|
} else {
|
|
plugin(root.L);
|
|
}
|
|
}(this, function (L) {
|
|
// Plugin is already added to Leaflet
|
|
if (L.Hotline) {
|
|
return L;
|
|
}
|
|
|
|
/**
|
|
* Core renderer.
|
|
* @constructor
|
|
* @param {HTMLElement | string} canvas - <canvas> element or its id
|
|
* to initialize the instance on.
|
|
*/
|
|
var Hotline = function (canvas) {
|
|
if (!(this instanceof Hotline)) { return new Hotline(canvas); }
|
|
|
|
var defaultPalette = {
|
|
0.0: 'green',
|
|
0.5: 'yellow',
|
|
1.0: 'red'
|
|
};
|
|
|
|
this._canvas = canvas = typeof canvas === 'string'
|
|
? document.getElementById(canvas)
|
|
: canvas;
|
|
|
|
this._ctx = canvas.getContext('2d');
|
|
this._width = canvas.width;
|
|
this._height = canvas.height;
|
|
|
|
this._weight = 5;
|
|
this._outlineWidth = 1;
|
|
this._outlineColor = 'black';
|
|
|
|
this._min = 0;
|
|
this._max = 1;
|
|
|
|
this._data = [];
|
|
|
|
this.palette(defaultPalette);
|
|
};
|
|
|
|
Hotline.prototype = {
|
|
/**
|
|
* Sets the width of the canvas. Used when clearing the canvas.
|
|
* @param {number} width - Width of the canvas.
|
|
*/
|
|
width: function (width) {
|
|
this._width = width;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the height of the canvas. Used when clearing the canvas.
|
|
* @param {number} height - Height of the canvas.
|
|
*/
|
|
height: function (height) {
|
|
this._height = height;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the weight of the path.
|
|
* @param {number} weight - Weight of the path in px.
|
|
*/
|
|
weight: function (weight) {
|
|
this._weight = weight;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the width of the outline around the path.
|
|
* @param {number} outlineWidth - Width of the outline in px.
|
|
*/
|
|
outlineWidth: function (outlineWidth) {
|
|
this._outlineWidth = outlineWidth;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the color of the outline around the path.
|
|
* @param {string} outlineColor - A CSS color value.
|
|
*/
|
|
outlineColor: function (outlineColor) {
|
|
this._outlineColor = outlineColor;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the palette gradient.
|
|
* @param {Object.<number, string>} palette - Gradient definition.
|
|
* e.g. { 0.0: 'white', 1.0: 'black' }
|
|
*/
|
|
palette: function (palette) {
|
|
var canvas = document.createElement('canvas'),
|
|
ctx = canvas.getContext('2d'),
|
|
gradient = ctx.createLinearGradient(0, 0, 0, 256);
|
|
|
|
canvas.width = 1;
|
|
canvas.height = 256;
|
|
|
|
for (var i in palette) {
|
|
gradient.addColorStop(i, palette[i]);
|
|
}
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, 1, 256);
|
|
|
|
this._palette = ctx.getImageData(0, 0, 1, 256).data;
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the value used at the start of the palette gradient.
|
|
* @param {number} min
|
|
*/
|
|
min: function (min) {
|
|
this._min = min;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the value used at the end of the palette gradient.
|
|
* @param {number} max
|
|
*/
|
|
max: function (max) {
|
|
this._max = max;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* A path to rander as a hotline.
|
|
* @typedef Array.<{x:number, y:number, z:number}> Path - Array of x, y and z coordinates.
|
|
*/
|
|
|
|
/**
|
|
* Sets the data that gets drawn on the canvas.
|
|
* @param {(Path|Path[])} data - A single path or an array of paths.
|
|
*/
|
|
data: function (data) {
|
|
this._data = data;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Adds a path to the list of paths.
|
|
* @param {Path} path
|
|
*/
|
|
add: function (path) {
|
|
this._data.push(path);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Draws the currently set paths.
|
|
*/
|
|
draw: function () {
|
|
var ctx = this._ctx;
|
|
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.lineCap = 'round';
|
|
|
|
this._drawOutline(ctx);
|
|
this._drawHotline(ctx);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 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 {Array.<number>} The RGB values as an array [r, g, b]
|
|
*/
|
|
getRGBForValue: function (value) {
|
|
var valueRelative = Math.min(Math.max((value - this._min) / (this._max - this._min), 0), 0.999);
|
|
var paletteIndex = Math.floor(valueRelative * 256) * 4;
|
|
|
|
return [
|
|
this._palette[paletteIndex],
|
|
this._palette[paletteIndex + 1],
|
|
this._palette[paletteIndex + 2]
|
|
];
|
|
},
|
|
|
|
/**
|
|
* Draws the outline of the graphs.
|
|
* @private
|
|
*/
|
|
_drawOutline: function (ctx) {
|
|
var i, j, dataLength, path, pathLength, pointStart, pointEnd;
|
|
|
|
if (this._outlineWidth) {
|
|
for (i = 0, dataLength = this._data.length; i < dataLength; i++) {
|
|
path = this._data[i];
|
|
ctx.lineWidth = this._weight + 2 * this._outlineWidth;
|
|
|
|
for (j = 1, pathLength = path.length; j < pathLength; j++) {
|
|
pointStart = path[j - 1];
|
|
pointEnd = path[j];
|
|
|
|
ctx.strokeStyle = this._outlineColor;
|
|
ctx.beginPath();
|
|
ctx.moveTo(pointStart.x, pointStart.y);
|
|
ctx.lineTo(pointEnd.x, pointEnd.y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Draws the color encoded hotline of the graphs.
|
|
* @private
|
|
*/
|
|
_drawHotline: function (ctx) {
|
|
var i, j, dataLength, path, pathLength, pointStart, pointEnd,
|
|
gradient, gradientStartRGB, gradientEndRGB;
|
|
|
|
ctx.lineWidth = this._weight;
|
|
|
|
for (i = 0, dataLength = this._data.length; i < dataLength; i++) {
|
|
path = this._data[i];
|
|
|
|
for (j = 1, pathLength = path.length; j < pathLength; j++) {
|
|
pointStart = path[j - 1];
|
|
pointEnd = path[j];
|
|
|
|
// Create a gradient for each segment, pick start end end colors from palette gradient
|
|
gradient = ctx.createLinearGradient(pointStart.x, pointStart.y, pointEnd.x, pointEnd.y);
|
|
gradientStartRGB = this.getRGBForValue(pointStart.z);
|
|
gradientEndRGB = this.getRGBForValue(pointEnd.z);
|
|
gradient.addColorStop(0, 'rgb(' + gradientStartRGB.join(',') + ')');
|
|
gradient.addColorStop(1, 'rgb(' + gradientEndRGB.join(',') + ')');
|
|
|
|
ctx.strokeStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.moveTo(pointStart.x, pointStart.y);
|
|
ctx.lineTo(pointEnd.x, pointEnd.y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
var Renderer = L.Canvas.extend({
|
|
_initContainer: function () {
|
|
L.Canvas.prototype._initContainer.call(this);
|
|
this._hotline = new Hotline(this._container);
|
|
},
|
|
|
|
_update: function () {
|
|
L.Canvas.prototype._update.call(this);
|
|
this._hotline.width(this._container.width);
|
|
this._hotline.height(this._container.height);
|
|
},
|
|
|
|
_updatePoly: function (layer) {
|
|
if (!this._drawing) { return; }
|
|
|
|
var parts = layer._parts;
|
|
|
|
if (!parts.length) { return; }
|
|
|
|
this._updateOptions(layer);
|
|
|
|
this._hotline
|
|
.data(parts)
|
|
.draw();
|
|
},
|
|
|
|
_updateOptions: function (layer) {
|
|
if (layer.options.min != null) {
|
|
this._hotline.min(layer.options.min);
|
|
}
|
|
if (layer.options.max != null) {
|
|
this._hotline.max(layer.options.max);
|
|
}
|
|
if (layer.options.weight != null) {
|
|
this._hotline.weight(layer.options.weight);
|
|
}
|
|
if (layer.options.outlineWidth != null) {
|
|
this._hotline.outlineWidth(layer.options.outlineWidth);
|
|
}
|
|
if (layer.options.outlineColor != null) {
|
|
this._hotline.outlineColor(layer.options.outlineColor);
|
|
}
|
|
if (layer.options.palette) {
|
|
this._hotline.palette(layer.options.palette);
|
|
}
|
|
}
|
|
});
|
|
|
|
var renderer = function (options) {
|
|
return L.Browser.canvas ? new Renderer(options) : null;
|
|
};
|
|
|
|
|
|
var Util = {
|
|
/**
|
|
* This is just a copy of the original Leaflet version that support a third z coordinate.
|
|
* @see {@link http://leafletjs.com/reference.html#lineutil-clipsegment|Leaflet}
|
|
*/
|
|
clipSegment: function (a, b, bounds, useLastCode, round) {
|
|
var codeA = useLastCode ? this._lastCode : L.LineUtil._getBitCode(a, bounds),
|
|
codeB = L.LineUtil._getBitCode(b, bounds),
|
|
codeOut, p, newCode;
|
|
|
|
// save 2nd code to avoid calculating it on the next segment
|
|
this._lastCode = codeB;
|
|
|
|
while (true) {
|
|
// if a,b is inside the clip window (trivial accept)
|
|
if (!(codeA | codeB)) {
|
|
return [a, b];
|
|
// if a,b is outside the clip window (trivial reject)
|
|
} else if (codeA & codeB) {
|
|
return false;
|
|
// other cases
|
|
} else {
|
|
codeOut = codeA || codeB;
|
|
p = L.LineUtil._getEdgeIntersection(a, b, codeOut, bounds, round);
|
|
newCode = L.LineUtil._getBitCode(p, bounds);
|
|
|
|
if (codeOut === codeA) {
|
|
p.z = a.z;
|
|
a = p;
|
|
codeA = newCode;
|
|
} else {
|
|
p.z = b.z;
|
|
b = p;
|
|
codeB = newCode;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
L.Hotline = L.Polyline.extend({
|
|
statics: {
|
|
Renderer: Renderer,
|
|
renderer: renderer
|
|
},
|
|
|
|
options: {
|
|
renderer: renderer(),
|
|
min: 0,
|
|
max: 1,
|
|
palette: {
|
|
0.0: 'green',
|
|
0.5: 'yellow',
|
|
1.0: 'red'
|
|
},
|
|
weight: 5,
|
|
outlineColor: 'black',
|
|
outlineWidth: 1
|
|
},
|
|
|
|
getRGBForValue: function (value) {
|
|
return this._renderer._hotline.getRGBForValue(value);
|
|
},
|
|
|
|
/**
|
|
* Just like the Leaflet version, but with support for a z coordinate.
|
|
*/
|
|
_projectLatlngs: function (latlngs, result, projectedBounds) {
|
|
var flat = latlngs[0] instanceof L.LatLng,
|
|
len = latlngs.length,
|
|
i, ring;
|
|
|
|
if (flat) {
|
|
ring = [];
|
|
for (i = 0; i < len; i++) {
|
|
ring[i] = this._map.latLngToLayerPoint(latlngs[i]);
|
|
// Add the altitude of the latLng as the z coordinate to the point
|
|
ring[i].z = latlngs[i].alt;
|
|
projectedBounds.extend(ring[i]);
|
|
}
|
|
result.push(ring);
|
|
} else {
|
|
for (i = 0; i < len; i++) {
|
|
this._projectLatlngs(latlngs[i], result, projectedBounds);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Just like the Leaflet version, but uses `Util.clipSegment()`.
|
|
*/
|
|
_clipPoints: function () {
|
|
if (this.options.noClip) {
|
|
this._parts = this._rings;
|
|
return;
|
|
}
|
|
|
|
this._parts = [];
|
|
|
|
var parts = this._parts,
|
|
bounds = this._renderer._bounds,
|
|
i, j, k, len, len2, segment, points;
|
|
|
|
for (i = 0, k = 0, len = this._rings.length; i < len; i++) {
|
|
points = this._rings[i];
|
|
|
|
for (j = 0, len2 = points.length; j < len2 - 1; j++) {
|
|
segment = Util.clipSegment(points[j], points[j + 1], bounds, j, true);
|
|
|
|
if (!segment) { continue; }
|
|
|
|
parts[k] = parts[k] || [];
|
|
parts[k].push(segment[0]);
|
|
|
|
// if segment goes out of screen, or it's the last one, it's the end of the line part
|
|
if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) {
|
|
parts[k].push(segment[1]);
|
|
k++;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_clickTolerance: function () {
|
|
return this.options.weight / 2 + this.options.outlineWidth + (L.Browser.touch ? 10 : 0);
|
|
}
|
|
});
|
|
|
|
L.hotline = function (latlngs, options) {
|
|
return new L.Hotline(latlngs, options);
|
|
};
|
|
|
|
|
|
return L;
|
|
})); |