src/layers/hexgridHeatLayer.js
// Forked from https://github.com/kronick/HexgridHeatmap
var rbush = require('rbush');
var turf = {
center: require('@turf/center'),
hexGrid: require('@turf/hex-grid'),
destination: require('@turf/destination'),
distance: require('@turf/distance'),
};
/**
* Creates a hexgrid-based vector heatmap on the specified map.
* @constructor
* @param {Map} map - The map object that this heatmap should add itself to and track.
* @param {string} [layername=hexgrid-heatmap] - The layer name to use for the heatmap.
* @param {string} [addBefore] - Name of a layer to insert this heatmap underneath.
*/
function HexgridHeatmap(map, layername, addBefore) {
if(layername === undefined) layername = "hexgrid-heatmap";
this.map = map;
this.layername = layername;
this._setupLayers(layername, addBefore);
this._setupEvents();
// Set up an R-tree to look for coordinates as they are stored in GeoJSON Feature objects
this._tree = rbush(9,['["geometry"]["coordinates"][0]','["geometry"]["coordinates"][1]','["geometry"]["coordinates"][0]','["geometry"]["coordinates"][1]']);
this._intensity = 8;
this._spread = 0.1;
this._minCellIntensity = 0; // Drop out cells that have less than this intensity
this._maxPointIntensity = 20; // Don't let a single point have a greater weight than this
this._cellDensity = 1;
var thisthis = this;
this._checkUpdateCompleteClosure = function(e) { thisthis._checkUpdateComplete(e); }
this._calculatingGrid = false;
this._recalcWhenReady = false;
}
HexgridHeatmap.prototype = {
_setupLayers: function(layername, addBefore) {
this.map.addSource(layername, {
type: 'geojson',
data: { type: "FeatureCollection", features: [] }
});
this.map.addLayer({
'id': layername,
'type': 'fill',
'source': layername,
'paint': {
'fill-opacity': 1.0,
'fill-color': {
property: 'count',
stops: [
// Short rainbow blue
[0, "rgba(0,185,243,0)"],
[50, "rgba(0,185,243,0.24)"],
[130, "rgba(255,223,0,0.3)"],
[200, "rgba(255,105,0,0.3)"],
]
}
}
});
this.layer = this.map.getLayer(layername);
this.source = this.map.getSource(layername);
},
_setupEvents: function() {
var thisthis = this;
this.map.on("moveend", function() {
thisthis._updateGrid();
});
},
/**
* Set the data to visualize with this heatmap layer
* @param {FeatureCollection} data - A GeoJSON FeatureCollection containing data to visualize with this heatmap
* @public
*/
setData: function(data) {
// Re-build R-tree index
this._tree.clear();
this._tree.load(data.features);
},
/**
* Set how widely points affect their neighbors
* @param {number} spread - A good starting point is 0.1. Higher values will result in more blurred heatmaps, lower values will highlight individual points more strongly.
* @public
*/
setSpread: function(spread) {
this._spread = spread;
},
/**
* Set the intensity value for all points.
* @param {number} intensity - Setting this too low will result in no data displayed, setting it too high will result in an oversaturated map. The default is 8 so adjust up or down from there according to the density of your data.
* @public
*/
setIntensity: function(intensity) {
this._intensity = intensity;
},
/**
* Set custom stops for the heatmap color schem
* @param {array} stops - An array of `stops` in the format of the Mapbox GL Style Spec. Values should range from 0 to about 200, though you can control saturation by setting different values here.
*/
setColorStops: function(stops) {
if (this.layer)
this.layer.setPaintProperty("fill-color", {property: "count", stops: stops});
},
/**
* Set the hexgrid cell density
* @param {number} density - Values less than 1 will result in a decreased cell density from the default, values greater than 1 will result in increaded density/higher resolution. Setting this value too high will result in slow performance.
* @public
*/
setCellDensity: function(density) {
this._cellDensity = density;
},
/**
* Manually force an update to the heatmap
* You can call this method to manually force the heatmap to be redrawn. Use this after calling `setData()`, `setSpread()`, or `setIntensity()`
*/
update: function() {
this._updateGrid();
},
_generateGrid: function() {
// Rebuild grid
//var cellSize = Math.min(Math.max(1000/Math.pow(2,this.map.transform.zoom), 0.01), 0.1); // Constant screen size
var cellSize = Math.max(500/Math.pow(2,this.map.transform.zoom) / this._cellDensity, 0.01); // Constant screen size
// TODO: These extents don't work when the map is rotated
var extents = this.map.getBounds().toArray()
extents = [extents[0][0], extents[0][1], extents[1][0], extents[1][1]];
var hexgrid = turf.hexGrid(extents, cellSize, 'kilometers');
var sigma = this._spread;
var a = 1 / (sigma * Math.sqrt(2 * Math.PI));
var amplitude = this._intensity;
var cellsToSave = [];
var thisthis = this;
hexgrid.features.forEach(function(cell) {
var center = turf.center(cell);
var strength = 0;
var SW = turf.destination(center, sigma * 4, -135);
var NE = turf.destination(center, sigma * 4, 45);
var pois = thisthis._tree.search({
minX: SW.geometry.coordinates[0],
minY: SW.geometry.coordinates[1],
maxX: NE.geometry.coordinates[0],
maxY: NE.geometry.coordinates[1]
});
pois.forEach(function(poi) {
// TODO: Allow weight to be influenced by a property within the POI
var distance = turf.distance(center, poi);
var weighted = Math.min(Math.exp(-(distance * distance / (2 * sigma * sigma))) * a * amplitude, thisthis._maxPointIntensity);
strength += weighted;
});
cell.properties.count = strength;
if(cell.properties.count > thisthis._minCellIntensity) {
cellsToSave.push(cell);
}
});
hexgrid.features = cellsToSave;
return hexgrid;
},
_updateGrid: function() {
if(!this._calculatingGrid) {
this._calculatingGrid = true;
var hexgrid = this._generateGrid();
if(hexgrid != null) {
var thisthis = this;
this.source.on("data", this._checkUpdateCompleteClosure);
this.source.setData(hexgrid);
}
else {
this._calculatingGrid = false;
}
}
else {
this._recalcWhenReady = true;
}
},
_checkUpdateComplete: function(e) {
if(e.dataType == "source") {
this.source.off("data", this._checkUpdateCompleteClosure);
this._calculatingGrid = false;
if(this._recalcWhenReady) this._updateGrid();
}
}
};
module.exports = exports = HexgridHeatmap;