UNPKG

leaflet-canvaslayer-field

Version:

A set of layers using canvas to draw ASCIIGrid or GeoTIFF files. This includes a basic raster layer (*ScalaField*) and an animated layer for vector fields, such as wind or currents (*VectorFieldAnim*)

210 lines (179 loc) 6.65 kB
import Cell from '../Cell'; /** * ScalarField on canvas (a 'Raster') */ L.CanvasLayer.ScalarField = L.CanvasLayer.Field.extend({ options: { type: 'colormap', // [colormap|vector] color: null, // function colorFor(value) [e.g. chromajs.scale], interpolate: false, // Change to use interpolation vectorSize: 20, // only used if 'vector' arrowDirection: 'from' // [from|towards] }, initialize: function(scalarField, options) { L.CanvasLayer.Field.prototype.initialize.call( this, scalarField, options ); L.Util.setOptions(this, options); }, _defaultColorScale: function() { return chroma.scale(['white', 'black']).domain(this._field.range); }, setColor(f) { this.options.color = f; this.needRedraw(); }, /* eslint-disable no-unused-vars */ onDrawLayer: function(viewInfo) { if (!this.isVisible()) return; this._updateOpacity(); let r = this._getRendererMethod(); //console.time('onDrawLayer'); r(); //console.timeEnd('onDrawLayer'); }, /* eslint-enable no-unused-vars */ _getRendererMethod: function() { switch (this.options.type) { case 'colormap': return this._drawImage.bind(this); case 'vector': return this._drawArrows.bind(this); default: throw Error(`Unkwown renderer type: ${this.options.type}`); } }, _ensureColor: function() { if (this.options.color === null) { this.setColor(this._defaultColorScale()); } }, _showCanvas() { L.CanvasLayer.Field.prototype._showCanvas.call(this); this.needRedraw(); // TODO check spurious redraw (e.g. hide/show without moving map) }, /** * Draws the field in an ImageData and applying it with putImageData. * Used as a reference: http://geoexamples.com/d3-raster-tools-docs/code_samples/raster-pixels-page.html */ _drawImage: function() { this._ensureColor(); let ctx = this._getDrawingContext(); let width = this._canvas.width; let height = this._canvas.height; let img = ctx.createImageData(width, height); let data = img.data; this._prepareImageIn(data, width, height); ctx.putImageData(img, 0, 0); }, /** * Prepares the image in data, as array with RGBAs * [R1, G1, B1, A1, R2, G2, B2, A2...] * @private * @param {[[Type]]} data [[Description]] * @param {Numver} width * @param {Number} height */ _prepareImageIn(data, width, height) { let f = this.options.interpolate ? 'interpolatedValueAt' : 'valueAt'; let pos = 0; for (let j = 0; j < height; j++) { for (let i = 0; i < width; i++) { let pointCoords = this._map.containerPointToLatLng([i, j]); let lon = pointCoords.lng; let lat = pointCoords.lat; let v = this._field[f](lon, lat); // 'valueAt' | 'interpolatedValueAt' || TODO check some 'artifacts' if (v !== null) { let color = this._getColorFor(v); let [R, G, B, A] = color.rgba(); data[pos] = R; data[pos + 1] = G; data[pos + 2] = B; data[pos + 3] = parseInt(A * 255); // not percent in alpha but hex 0-255 } pos = pos + 4; } } }, /** * Draws the field as a set of arrows. Direction from 0 to 360 is assumed. */ _drawArrows: function() { const bounds = this._pixelBounds(); const pixelSize = (bounds.max.x - bounds.min.x) / this._field.nCols; var stride = Math.max( 1, Math.floor(1.2 * this.options.vectorSize / pixelSize) ); const ctx = this._getDrawingContext(); ctx.strokeStyle = this.options.color; var currentBounds = this._map.getBounds(); for (var y = 0; y < this._field.height; y = y + stride) { for (var x = 0; x < this._field.width; x = x + stride) { let [lon, lat] = this._field._lonLatAtIndexes(x, y); let v = this._field.valueAt(lon, lat); let center = L.latLng(lat, lon); if (v !== null && currentBounds.contains(center)) { let cell = new Cell( center, v, this.cellXSize, this.cellYSize ); this._drawArrow(cell, ctx); } } } }, _pixelBounds: function() { const bounds = this.getBounds(); const northWest = this._map.latLngToContainerPoint( bounds.getNorthWest() ); const southEast = this._map.latLngToContainerPoint( bounds.getSouthEast() ); var pixelBounds = L.bounds(northWest, southEast); return pixelBounds; }, _drawArrow: function(cell, ctx) { var projected = this._map.latLngToContainerPoint(cell.center); // colormap vs. simple color let color = this.options.color; if (typeof color === 'function') { ctx.strokeStyle = color(cell.value); } const size = this.options.vectorSize; ctx.save(); ctx.translate(projected.x, projected.y); let rotationRads = (90 + cell.value) * Math.PI / 180; // from, by default if (this.options.arrowDirection === 'towards') { rotationRads = rotationRads + Math.PI; } ctx.rotate(rotationRads); ctx.beginPath(); ctx.moveTo(-size / 2, 0); ctx.lineTo(+size / 2, 0); ctx.moveTo(size * 0.25, -size * 0.25); ctx.lineTo(+size / 2, 0); ctx.lineTo(size * 0.25, size * 0.25); ctx.stroke(); ctx.restore(); }, /** * Gets a chroma color for a pixel value, according to 'options.color' */ _getColorFor(v) { let c = this.options.color; // e.g. for a constant 'red' if (typeof c === 'function') { c = this.options.color(v); } let color = chroma(c); // to be more flexible, a chroma color object is always created || TODO improve efficiency return color; } }); L.canvasLayer.scalarField = function(scalarField, options) { return new L.CanvasLayer.ScalarField(scalarField, options); };