heatmap-leaflet.js 6.77 KB
/*
 * heatmap.js 0.2 Leaflet overlay
 *
 * Copyright (c) 2012, Dominik Moritz
 * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license.
 *
 * Attribution
 *  - Some snippets for canvas layer: https://gist.github.com/2566567
 *  - QuadTree: https://github.com/jsmarkus/ExamplesByMesh/tree/master/JavaScript/QuadTree
 */

 L.TileLayer.HeatMap = L.TileLayer.Canvas.extend({
	options: {
        debug: false,
        opacity: 0.9,  // opactity is between 0 and 1, not in percent
        radius: {
            value: 20,
            absolute: false  // true: radius in meters, false: radius in pixels
        }
	},

	initialize: function (options, data) {
        var self = this;
        L.Util.setOptions(this, options);

        this.drawTile = function (tile, tilePoint, zoom) {
            var ctx = {
                canvas: tile,
                tilePoint: tilePoint,
                zoom: zoom
            };

            if (self.options.debug) {
                self._drawDebugInfo(ctx);
            }
            this._draw(ctx);
        };
    },

    _drawDebugInfo: function (ctx) {
        var canvas = L.DomUtil.create('canvas', 'leaflet-tile-debug');
        var tileSize = this.options.tileSize;
        canvas.width = tileSize;
        canvas.height = tileSize;
        ctx.canvas.appendChild(canvas);
        ctx.dbgcanvas = canvas;

        var max = tileSize;
        var g = canvas.getContext('2d');
        g.strokeStyle = '#000000';
        g.fillStyle = '#FFFF00';
        g.strokeRect(0, 0, max, max);
        g.font = "12px Arial";
        g.fillRect(0, 0, 5, 5);
        g.fillRect(0, max - 5, 5, 5);
        g.fillRect(max - 5, 0, 5, 5);
        g.fillRect(max - 5, max - 5, 5, 5);
        g.fillRect(max / 2 - 5, max / 2 - 5, 10, 10);
        g.strokeText(ctx.tilePoint.x + ' ' + ctx.tilePoint.y + ' ' + ctx.zoom, max / 2 - 30, max / 2 - 10);

        this._drawPoint(ctx, [0,0]);
    },

    /*
     * Used for debug
     */
    _drawPoint: function (ctx, geom) {
        var p = this._tilePoint(ctx, geom);
        var c = ctx.dbgcanvas;
        var g = c.getContext('2d');
        g.beginPath();
        g.fillStyle = '#FF0000';
        g.arc(p.x, p.y, 4, 0, Math.PI * 2);
        g.closePath();
        g.fill();
        g.restore();
    },

    _createTileProto: function () {
        var proto = this._canvasProto = L.DomUtil.create('div', 'leaflet-tile');

        var tileSize = this.options.tileSize;
        proto.style.width = tileSize+"px";
        proto.style.height = tileSize+"px";
        proto.width = tileSize;
        proto.height = tileSize;
    },

    /**
     * Inserts data into quadtree and redraws heatmap canvas
     */
    setData: function(dataset) {
        var self = this;
        var latLngs = [];
        this._maxValue = 0;
        dataset.forEach(function(d) {
            latLngs.push(new L.LatLng(d.lat, d.lon));
            self._maxValue = Math.max(self._maxValue, d.value);
        });
        this._bounds = new L.LatLngBounds(latLngs);

        this._quad = new QuadTree(this._boundsToQuery(this._bounds), false, 6, 6);

        dataset.forEach(function(d) {
            self._quad.insert({
                x: d.lon,
                y: d.lat,
                value: d.value
            });
        });
        this.redraw();
    },

    /**
     * Transforms coordinates to tile space
     */
    _tilePoint: function (ctx, coords) {
        // start coords to tile 'space'
        var s = ctx.tilePoint.multiplyBy(this.options.tileSize);

        // actual coords to tile 'space'
        var p = this._map.project(new L.LatLng(coords[1], coords[0]));

        // point to draw
        var x = Math.round(p.x - s.x);
        var y = Math.round(p.y - s.y);
        return [x, y];
    },

    /**
     * Creates a query for the quadtree from bounds
     */
    _boundsToQuery: function(bounds) {
        return {
            x: bounds.getSouthWest().lng,
            y: bounds.getSouthWest().lat,
            width: bounds.getNorthEast().lng-bounds.getSouthWest().lng,
            height: bounds.getNorthEast().lat-bounds.getSouthWest().lat
        };
    },

    _getLatRadius: function () {
        return (this.options.radius.value / 40075017) * 360;
    },

    _getLngRadius: function (point) {
        return this._getLatRadius() / Math.cos(L.LatLng.DEG_TO_RAD * point.lat);
    },

    /*
     * The idea is to create two points and then get
     * the distance between the two in order to know what
     * the absolute radius in this tile could be.
     */
    projectLatlngs: function (point) {
        var lngRadius = this._getLngRadius(point),
            latlng2 = new L.LatLng(point.lat, point.lng - lngRadius, true),
            p = this._map.latLngToLayerPoint(latlng2),
            q = this._map.latLngToLayerPoint(point);
        return Math.max(Math.round(q.x - p.x), 1);
    },

    _draw: function (ctx) {
        if (!this._quad || !this._map) {
            return;
        }

        var self = this,
            options = this.options,
            tile = ctx.canvas,
            tileSize = options.tileSize,
            radiusValue = this.options.radius.value;

        var localXY, value, pointsInTile = [];

        var nwPoint = ctx.tilePoint.multiplyBy(tileSize),
            sePoint = nwPoint.add(new L.Point(tileSize, tileSize));

        // Set the radius for the tile, if necessary.
        // The radius of a circle can be either absolute in pixels or in meters
        // The radius in pixels is not the same on the whole map.
        if (options.radius.absolute) {
            var centerPoint = nwPoint.add(new L.Point(tileSize/2, tileSize/2));
            var p = this._map.unproject(centerPoint);
            radiusValue = this.projectLatlngs(p);
        }

        var heatmap = h337.create({
            "radius": radiusValue,
            "element": tile,
            "visible": true,
            "opacity": 100,  // we use leaflet's opacity for tiles
            "gradient": options.gradient,
            "debug": options.debug
        });

        // padding
        var pad = new L.Point(radiusValue, radiusValue);
        nwPoint = nwPoint.subtract(pad);
        sePoint = sePoint.add(pad);

        var bounds = new L.LatLngBounds(this._map.unproject(sePoint), this._map.unproject(nwPoint));
        this._quad.retrieveInBounds(this._boundsToQuery(bounds)).forEach(function(obj) {
            localXY = self._tilePoint(ctx, [obj.x, obj.y]);
            value = obj.value;
            pointsInTile.push({
                x: localXY[0],
                y: localXY[1],
                count: value
            });
        });

        heatmap.store.setDataSet({max: this._maxValue, data: pointsInTile});

        return this;
    }
});

L.TileLayer.heatMap = function (options) {
    return new L.TileLayer.HeatMap(options);
};