-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathHexgridHeatmap.js
207 lines (176 loc) · 7.06 KB
/
HexgridHeatmap.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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.addLayer({
'id': layername,
'type': 'fill',
'source': {
type: 'geojson',
data: { type: "FeatureCollection", features: [] }
},
'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)"],
]
}
}
}, addBefore);
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) {
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;