UNPKG

leaflet-advanced-heatmap

Version:
302 lines (245 loc) 7.48 kB
import AdvancedHeatMap from 'advanced-heatmap' import L, { Layer } from 'leaflet' import merge from 'lodash/merge' /** * official heatmap (https://github.com/Leaflet/Leaflet.heat) * makes canvas for whole page * - cons: move or any tiny update leads to complete redraw whole canvas * - pros: we don't need to solve local-global problem by filtering visible items * * https://github.com/pa7/heatmap.js/tree/master/plugins/leaflet-heatmap * - pros + cons: same as the official heatmap - uses a single canvas * * * # TODO: * * - DONE: scale radius of points based on map scale * - ?: extend Renderer * - CANCEL: support retina if (Browser.retina) { this._ctx.scale(2, 2); } * - we may not need it because heat map are blur by nature * - check heatmap animation * - webgl and fallback to canvas 2d * - performance: reuse previous canvas */ const defaultHeatmapOptions = { smooth: 0.5, valueToOpacity: true, // default radius in Meters radius: 500, // default opacity opacity: 1, features: { radius: { source: { idx: 2, // TODO: setup min: 1, max: 32 }, // TODO: setup value: { min: 1, max: 32 } }, alpha: { source: { idx: 3, min: 0, max: 1 }, value: { min: 0.1, max: 0.8 } } } } export default class AdvancedHeatmapLayer extends Layer { constructor (...args) { super(...args) this._scaleByMeters = this._scaleByMeters.bind(this) this._scaleByPixels = this._scaleByPixels.bind(this) this._handleMoveStart = this._handleMoveStart.bind(this) this._handleMoveEnd = this._handleMoveEnd.bind(this) } initialize (latlngs, options) { this._latlngs = latlngs L.setOptions(this, options) } setLatLngs (latlngs) { this._latlngs = latlngs return this.redraw() } addLatLng (latlng) { if (!this._latlngs) { this._latlngs = [latlng] } else { this._latlngs.push(latlng) } return this.redraw() } setOptions (options) { L.setOptions(this, options) if (this._heat) { this._updateOptions() } return this.redraw() } redraw () { if (this._heat && !this._frame && this._map && !this._map._animating && !this._isMoving) { this._frame = L.Util.requestAnimFrame(this._redraw, this) } return this } onAdd (map) { this._map = map if (!this._canvas) { this._initCanvas() } if (this.options.pane) { this.getPane().appendChild(this._canvas) } else { map._panes.overlayPane.appendChild(this._canvas) } map.on('moveend', this._reset, this) if (map.options.zoomAnimation && L.Browser.any3d) { map.on('zoomanim', this._animateZoom, this) } map.on('move', this._handleMoveStart, this) map.on('moveend', this._handleMoveEnd, this) this._reset() } _handleMoveStart () { this._isMoving = true } _handleMoveEnd () { this._isMoving = false } onRemove (map) { if (this.options.pane) { this.getPane().removeChild(this._canvas) } else { map.getPanes().overlayPane.removeChild(this._canvas) } map.off('moveend', this._reset, this) if (map.options.zoomAnimation) { map.off('zoomanim', this._animateZoom, this) } map.off('move', this._handleMoveStart, this) map.off('moveend', this._handleMoveEnd, this) } addTo (map) { map.addLayer(this) return this } _initCanvas () { const canvas = this._canvas = L.DomUtil.create('canvas', 'leaflet-heatmap-layer leaflet-layer') const originProp = L.DomUtil.testProp(['transformOrigin', 'WebkitTransformOrigin', 'msTransformOrigin']) canvas.style[originProp] = '50% 50%' const size = this._map.getSize() canvas.width = size.x canvas.height = size.y const animated = this._map.options.zoomAnimation && L.Browser.any3d L.DomUtil.addClass(canvas, 'leaflet-zoom-' + (animated ? 'animated' : 'hide')) this.options = merge({}, defaultHeatmapOptions, this.options.heatmap) this._heat = new AdvancedHeatMap({ canvas, ...this.options }) this._updateOptions() } _updateOptions () { // TODO: // this._heat.radius(this.options.radius || this._heat.defaultRadius, this.options.blur); // // if (this.options.gradient) { // this._heat.gradient(this.options.gradient); // } // if (this.options.max) { // this._heat.max(this.options.max); // } } _reset () { let topLeft = this._map.containerPointToLayerPoint([0, 0]) L.DomUtil.setPosition(this._canvas, topLeft) // let size = this._map.getSize() // TODO: // if (this._heat._width !== size.x) { // this._canvas.width = this._heat._width = size.x; // } // if (this._heat._height !== size.y) { // this._canvas.height = this._heat._height = size.y; // } this._redraw() } _redraw () { if (!this._map) { return } const maxR = 100 const size = this._map.getSize() const bounds = new L.Bounds( L.point([-maxR, -maxR]), size.add([maxR, maxR])) // console.time('prepare data') // TODO: another option is this._scaleByPixels const scaleBy = this._scaleByMeters // TODO: could we be more efficient and update changes only? this._heat.removeAll() for (let i = 0, len = this._latlngs && this._latlngs.length; i < len; i++) { const pointSource = this._latlngs[i] const latLng = new L.LatLng(...pointSource) const p = this._map.latLngToContainerPoint(latLng) if (bounds.contains(p)) { // scale based on lng const radius = scaleBy( latLng, // TODO: for the moment size is 3rd field and 4th is opacity // but it could be different pointSource[2] || this.options.radius ) this._heat.add([ Math.round(p.x), Math.round(p.y), radius, pointSource[3] || this.options.opacity ]) } } // console.timeEnd('prepare data') // console.time('draw ' + data.length); // this._heat.data(data).draw(this.options.minOpacity); // console.time('render') this._heat.render().then(() => { // console.timeEnd('render') }) // console.timeEnd('draw ' + data.length); this._frame = null } _scaleByPixels (latlng, size = 1) { return size } _scaleByMeters (latlng, size = 1) { // necessary to maintain accurately sized circles // to change scale to miles (for example), you will need to convert 40075017 (equatorial circumference of the Earth // in metres) to miles const map = this._map const lngRadius = (size / 40075017) * 360 / Math.cos((Math.PI / 180) * latlng.lat) const latlng2 = new L.LatLng(latlng.lat, latlng.lng - lngRadius) const point = map.latLngToLayerPoint(latlng) const point2 = map.latLngToLayerPoint(latlng2) return Math.max(Math.round(point.x - point2.x), 1) } _animateZoom (e) { const scale = this._map.getZoomScale(e.zoom) const offset = this._map._getCenterOffset(e.center)._multiplyBy(-scale).subtract(this._map._getMapPanePos()) if (L.DomUtil.setTransform) { L.DomUtil.setTransform(this._canvas, offset, scale) } else { this._canvas.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(offset) + ' scale(' + scale + ')' } } }