├── HexgridHeatmap.js ├── README.md ├── dist └── HexgridHeatmap.js ├── docs ├── .DS_Store └── hexgrid-heatmap.png ├── example ├── index.html └── sightseeing-pois.js ├── index.js └── package.json /HexgridHeatmap.js: -------------------------------------------------------------------------------- 1 | var rbush = require('rbush'); 2 | var turf = { 3 | center: require('@turf/center'), 4 | hexGrid: require('@turf/hex-grid'), 5 | destination: require('@turf/destination'), 6 | distance: require('@turf/distance'), 7 | }; 8 | /** 9 | * Creates a hexgrid-based vector heatmap on the specified map. 10 | * @constructor 11 | * @param {Map} map - The map object that this heatmap should add itself to and track. 12 | * @param {string} [layername=hexgrid-heatmap] - The layer name to use for the heatmap. 13 | * @param {string} [addBefore] - Name of a layer to insert this heatmap underneath. 14 | */ 15 | function HexgridHeatmap(map, layername, addBefore) { 16 | if(layername === undefined) layername = "hexgrid-heatmap"; 17 | this.map = map; 18 | this.layername = layername; 19 | this._setupLayers(layername, addBefore); 20 | this._setupEvents(); 21 | // Set up an R-tree to look for coordinates as they are stored in GeoJSON Feature objects 22 | this._tree = rbush(9,['["geometry"]["coordinates"][0]','["geometry"]["coordinates"][1]','["geometry"]["coordinates"][0]','["geometry"]["coordinates"][1]']); 23 | 24 | this._intensity = 8; 25 | this._spread = 0.1; 26 | this._minCellIntensity = 0; // Drop out cells that have less than this intensity 27 | this._maxPointIntensity = 20; // Don't let a single point have a greater weight than this 28 | this._cellDensity = 1; 29 | 30 | var thisthis = this; 31 | this._checkUpdateCompleteClosure = function(e) { thisthis._checkUpdateComplete(e); } 32 | this._calculatingGrid = false; 33 | this._recalcWhenReady = false; 34 | } 35 | 36 | HexgridHeatmap.prototype = { 37 | _setupLayers: function(layername, addBefore) { 38 | this.map.addLayer({ 39 | 'id': layername, 40 | 'type': 'fill', 41 | 'source': { 42 | type: 'geojson', 43 | data: { type: "FeatureCollection", features: [] } 44 | }, 45 | 'paint': { 46 | 'fill-opacity': 1.0, 47 | 'fill-color': { 48 | property: 'count', 49 | stops: [ 50 | // Short rainbow blue 51 | [0, "rgba(0,185,243,0)"], 52 | [50, "rgba(0,185,243,0.24)"], 53 | [130, "rgba(255,223,0,0.3)"], 54 | [200, "rgba(255,105,0,0.3)"], 55 | ] 56 | } 57 | } 58 | }, addBefore); 59 | 60 | this.layer = this.map.getLayer(layername); 61 | this.source = this.map.getSource(layername); 62 | }, 63 | _setupEvents: function() { 64 | var thisthis = this; 65 | this.map.on("moveend", function() { 66 | thisthis._updateGrid(); 67 | }); 68 | }, 69 | 70 | 71 | /** 72 | * Set the data to visualize with this heatmap layer 73 | * @param {FeatureCollection} data - A GeoJSON FeatureCollection containing data to visualize with this heatmap 74 | * @public 75 | */ 76 | setData: function(data) { 77 | // Re-build R-tree index 78 | this._tree.clear(); 79 | this._tree.load(data.features); 80 | }, 81 | 82 | 83 | /** 84 | * Set how widely points affect their neighbors 85 | * @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. 86 | * @public 87 | */ 88 | setSpread: function(spread) { 89 | this._spread = spread; 90 | }, 91 | 92 | 93 | /** 94 | * Set the intensity value for all points. 95 | * @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. 96 | * @public 97 | */ 98 | setIntensity: function(intensity) { 99 | this._intensity = intensity; 100 | }, 101 | 102 | 103 | /** 104 | * Set custom stops for the heatmap color schem 105 | * @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. 106 | */ 107 | setColorStops: function(stops) { 108 | this.layer.setPaintProperty("fill-color", {property: "count", stops: stops}); 109 | }, 110 | 111 | 112 | /** 113 | * Set the hexgrid cell density 114 | * @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. 115 | * @public 116 | */ 117 | setCellDensity: function(density) { 118 | this._cellDensity = density; 119 | }, 120 | 121 | 122 | /** 123 | * Manually force an update to the heatmap 124 | * You can call this method to manually force the heatmap to be redrawn. Use this after calling `setData()`, `setSpread()`, or `setIntensity()` 125 | */ 126 | update: function() { 127 | this._updateGrid(); 128 | }, 129 | 130 | 131 | _generateGrid: function() { 132 | // Rebuild grid 133 | //var cellSize = Math.min(Math.max(1000/Math.pow(2,this.map.transform.zoom), 0.01), 0.1); // Constant screen size 134 | 135 | var cellSize = Math.max(500/Math.pow(2,this.map.transform.zoom) / this._cellDensity, 0.01); // Constant screen size 136 | 137 | // TODO: These extents don't work when the map is rotated 138 | var extents = this.map.getBounds().toArray() 139 | extents = [extents[0][0], extents[0][1], extents[1][0], extents[1][1]]; 140 | 141 | var hexgrid = turf.hexGrid(extents, cellSize, 'kilometers'); 142 | 143 | var sigma = this._spread; 144 | var a = 1 / (sigma * Math.sqrt(2 * Math.PI)); 145 | var amplitude = this._intensity; 146 | 147 | var cellsToSave = []; 148 | 149 | var thisthis = this; 150 | hexgrid.features.forEach(function(cell) { 151 | var center = turf.center(cell); 152 | var strength = 0; 153 | var SW = turf.destination(center, sigma * 4, -135); 154 | var NE = turf.destination(center, sigma * 4, 45); 155 | var pois = thisthis._tree.search({ 156 | minX: SW.geometry.coordinates[0], 157 | minY: SW.geometry.coordinates[1], 158 | maxX: NE.geometry.coordinates[0], 159 | maxY: NE.geometry.coordinates[1] 160 | }); 161 | 162 | pois.forEach(function(poi) { 163 | // TODO: Allow weight to be influenced by a property within the POI 164 | var distance = turf.distance(center, poi); 165 | 166 | var weighted = Math.min(Math.exp(-(distance * distance / (2 * sigma * sigma))) * a * amplitude, thisthis._maxPointIntensity); 167 | strength += weighted; 168 | }); 169 | 170 | cell.properties.count = strength; 171 | 172 | if(cell.properties.count > thisthis._minCellIntensity) { 173 | cellsToSave.push(cell); 174 | } 175 | }); 176 | 177 | hexgrid.features = cellsToSave; 178 | return hexgrid; 179 | 180 | }, 181 | _updateGrid: function() { 182 | if(!this._calculatingGrid) { 183 | this._calculatingGrid = true; 184 | var hexgrid = this._generateGrid(); 185 | if(hexgrid != null) { 186 | var thisthis = this; 187 | this.source.on("data", this._checkUpdateCompleteClosure); 188 | this.source.setData(hexgrid); 189 | } 190 | else { 191 | this._calculatingGrid = false; 192 | } 193 | } 194 | else { 195 | this._recalcWhenReady = true; 196 | } 197 | }, 198 | _checkUpdateComplete: function(e) { 199 | if(e.dataType == "source") { 200 | this.source.off("data", this._checkUpdateCompleteClosure); 201 | this._calculatingGrid = false; 202 | if(this._recalcWhenReady) this._updateGrid(); 203 | } 204 | } 205 | }; 206 | 207 | module.exports = exports = HexgridHeatmap; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Hexgrid-based vector heatmaps for your Mapbox GL JS Maps 2 | 3 | ![](https://github.com/kronick/HexgridHeatmap/raw/master/docs/hexgrid-heatmap.png) 4 | 5 | 6 | ### Usage 7 | See the [examples directory](examples/index.html) for an example of usage. You will need to provide your own Mabox access token to view the map. 8 | 9 | Here's how it works: 10 | 11 | - Create an instance of `HexgridHeatmap`. It will automatically add the necessary layer and events to the specified map: 12 | 13 | ``` 14 | var heatmap = new HexgridHeatmap(map, "hexgrid-heatmap", "waterway-label"); 15 | ``` 16 | 17 | - You can control the intensity and spread of the points: 18 | 19 | ``` 20 | heatmap.setIntensity(15); 21 | heatmap.setSpread(0.3); 22 | ``` 23 | 24 | - Feed in a GeoJSON `FeatureCollection` full of points to map 25 | 26 | ``` 27 | heatmap.setData(sightseeingPOIs); 28 | ``` 29 | 30 | - Manually call `update()` after changing settings. The heatmap will automatically call update() when moving and zooming the map. 31 | 32 | ``` 33 | heatmap.update(); 34 | ``` 35 | 36 | - You can set your own color palette using stops conforming to the Mapbox GL style spec: 37 | 38 | ``` 39 | heatmap.setColorStops([ 40 | [0, "rgba(0,185,243,0)"], 41 | [50, "rgba(0,185,243,0.5)"], 42 | [130, "rgba(255,223,0,0.6)"], 43 | [200, "rgba(255,105,0,0.6)"] 44 | ]); 45 | ``` 46 | -------------------------------------------------------------------------------- /dist/HexgridHeatmap.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o thisthis._minCellIntensity) { 174 | cellsToSave.push(cell); 175 | } 176 | }); 177 | 178 | hexgrid.features = cellsToSave; 179 | return hexgrid; 180 | 181 | }, 182 | _updateGrid: function() { 183 | if(!this._calculatingGrid) { 184 | this._calculatingGrid = true; 185 | var hexgrid = this._generateGrid(); 186 | if(hexgrid != null) { 187 | var thisthis = this; 188 | this.source.on("data", this._checkUpdateCompleteClosure); 189 | this.source.setData(hexgrid); 190 | } 191 | else { 192 | this._calculatingGrid = false; 193 | } 194 | } 195 | else { 196 | this._recalcWhenReady = true; 197 | } 198 | }, 199 | _checkUpdateComplete: function(e) { 200 | if(e.dataType == "source") { 201 | this.source.off("data", this._checkUpdateCompleteClosure); 202 | this._calculatingGrid = false; 203 | if(this._recalcWhenReady) this._updateGrid(); 204 | } 205 | } 206 | }; 207 | 208 | module.exports = exports = HexgridHeatmap; 209 | },{"@turf/center":4,"@turf/destination":5,"@turf/distance":6,"@turf/hex-grid":8,"rbush":12}],2:[function(require,module,exports){ 210 | window.HexgridHeatmap = require('./HexgridHeatmap'); 211 | },{"./HexgridHeatmap":1}],3:[function(require,module,exports){ 212 | var each = require('@turf/meta').coordEach; 213 | 214 | /** 215 | * Takes a set of features, calculates the bbox of all input features, and returns a bounding box. 216 | * 217 | * @name bbox 218 | * @param {(Feature|FeatureCollection)} geojson input features 219 | * @return {Array} bbox extent in [minX, minY, maxX, maxY] order 220 | * @example 221 | * var pt1 = turf.point([114.175329, 22.2524]) 222 | * var pt2 = turf.point([114.170007, 22.267969]) 223 | * var pt3 = turf.point([114.200649, 22.274641]) 224 | * var pt4 = turf.point([114.200649, 22.274641]) 225 | * var pt5 = turf.point([114.186744, 22.265745]) 226 | * var features = turf.featureCollection([pt1, pt2, pt3, pt4, pt5]) 227 | * 228 | * var bbox = turf.bbox(features); 229 | * 230 | * var bboxPolygon = turf.bboxPolygon(bbox); 231 | * 232 | * //=bbox 233 | * 234 | * //=bboxPolygon 235 | */ 236 | module.exports = function (geojson) { 237 | var bbox = [Infinity, Infinity, -Infinity, -Infinity]; 238 | each(geojson, function (coord) { 239 | if (bbox[0] > coord[0]) bbox[0] = coord[0]; 240 | if (bbox[1] > coord[1]) bbox[1] = coord[1]; 241 | if (bbox[2] < coord[0]) bbox[2] = coord[0]; 242 | if (bbox[3] < coord[1]) bbox[3] = coord[1]; 243 | }); 244 | return bbox; 245 | }; 246 | 247 | },{"@turf/meta":10}],4:[function(require,module,exports){ 248 | var bbox = require('@turf/bbox'), 249 | point = require('@turf/helpers').point; 250 | 251 | /** 252 | * Takes a {@link Feature} or {@link FeatureCollection} and returns the absolute center point of all features. 253 | * 254 | * @name center 255 | * @param {(Feature|FeatureCollection)} layer input features 256 | * @return {Feature} a Point feature at the absolute center point of all input features 257 | * @example 258 | * var features = { 259 | * "type": "FeatureCollection", 260 | * "features": [ 261 | * { 262 | * "type": "Feature", 263 | * "properties": {}, 264 | * "geometry": { 265 | * "type": "Point", 266 | * "coordinates": [-97.522259, 35.4691] 267 | * } 268 | * }, { 269 | * "type": "Feature", 270 | * "properties": {}, 271 | * "geometry": { 272 | * "type": "Point", 273 | * "coordinates": [-97.502754, 35.463455] 274 | * } 275 | * }, { 276 | * "type": "Feature", 277 | * "properties": {}, 278 | * "geometry": { 279 | * "type": "Point", 280 | * "coordinates": [-97.508269, 35.463245] 281 | * } 282 | * }, { 283 | * "type": "Feature", 284 | * "properties": {}, 285 | * "geometry": { 286 | * "type": "Point", 287 | * "coordinates": [-97.516809, 35.465779] 288 | * } 289 | * }, { 290 | * "type": "Feature", 291 | * "properties": {}, 292 | * "geometry": { 293 | * "type": "Point", 294 | * "coordinates": [-97.515372, 35.467072] 295 | * } 296 | * }, { 297 | * "type": "Feature", 298 | * "properties": {}, 299 | * "geometry": { 300 | * "type": "Point", 301 | * "coordinates": [-97.509363, 35.463053] 302 | * } 303 | * }, { 304 | * "type": "Feature", 305 | * "properties": {}, 306 | * "geometry": { 307 | * "type": "Point", 308 | * "coordinates": [-97.511123, 35.466601] 309 | * } 310 | * }, { 311 | * "type": "Feature", 312 | * "properties": {}, 313 | * "geometry": { 314 | * "type": "Point", 315 | * "coordinates": [-97.518547, 35.469327] 316 | * } 317 | * }, { 318 | * "type": "Feature", 319 | * "properties": {}, 320 | * "geometry": { 321 | * "type": "Point", 322 | * "coordinates": [-97.519706, 35.469659] 323 | * } 324 | * }, { 325 | * "type": "Feature", 326 | * "properties": {}, 327 | * "geometry": { 328 | * "type": "Point", 329 | * "coordinates": [-97.517839, 35.466998] 330 | * } 331 | * }, { 332 | * "type": "Feature", 333 | * "properties": {}, 334 | * "geometry": { 335 | * "type": "Point", 336 | * "coordinates": [-97.508678, 35.464942] 337 | * } 338 | * }, { 339 | * "type": "Feature", 340 | * "properties": {}, 341 | * "geometry": { 342 | * "type": "Point", 343 | * "coordinates": [-97.514914, 35.463453] 344 | * } 345 | * } 346 | * ] 347 | * }; 348 | * 349 | * var centerPt = turf.center(features); 350 | * centerPt.properties['marker-size'] = 'large'; 351 | * centerPt.properties['marker-color'] = '#000'; 352 | * 353 | * var resultFeatures = features.features.concat(centerPt); 354 | * var result = { 355 | * "type": "FeatureCollection", 356 | * "features": resultFeatures 357 | * }; 358 | * 359 | * //=result 360 | */ 361 | 362 | module.exports = function (layer) { 363 | var ext = bbox(layer); 364 | var x = (ext[0] + ext[2]) / 2; 365 | var y = (ext[1] + ext[3]) / 2; 366 | return point([x, y]); 367 | }; 368 | 369 | },{"@turf/bbox":3,"@turf/helpers":7}],5:[function(require,module,exports){ 370 | //http://en.wikipedia.org/wiki/Haversine_formula 371 | //http://www.movable-type.co.uk/scripts/latlong.html 372 | var getCoord = require('@turf/invariant').getCoord; 373 | var helpers = require('@turf/helpers'); 374 | var point = helpers.point; 375 | var distanceToRadians = helpers.distanceToRadians; 376 | 377 | /** 378 | * Takes a {@link Point} and calculates the location of a destination point given a distance in degrees, radians, miles, or kilometers; and bearing in degrees. This uses the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula) to account for global curvature. 379 | * 380 | * @name destination 381 | * @param {Feature} from starting point 382 | * @param {number} distance distance from the starting point 383 | * @param {number} bearing ranging from -180 to 180 384 | * @param {string} [units=kilometers] miles, kilometers, degrees, or radians 385 | * @returns {Feature} destination point 386 | * @example 387 | * var point = { 388 | * "type": "Feature", 389 | * "properties": { 390 | * "marker-color": "#0f0" 391 | * }, 392 | * "geometry": { 393 | * "type": "Point", 394 | * "coordinates": [-75.343, 39.984] 395 | * } 396 | * }; 397 | * var distance = 50; 398 | * var bearing = 90; 399 | * var units = 'miles'; 400 | * 401 | * var destination = turf.destination(point, distance, bearing, units); 402 | * destination.properties['marker-color'] = '#f00'; 403 | * 404 | * var result = { 405 | * "type": "FeatureCollection", 406 | * "features": [point, destination] 407 | * }; 408 | * 409 | * //=result 410 | */ 411 | module.exports = function (from, distance, bearing, units) { 412 | var degrees2radians = Math.PI / 180; 413 | var radians2degrees = 180 / Math.PI; 414 | var coordinates1 = getCoord(from); 415 | var longitude1 = degrees2radians * coordinates1[0]; 416 | var latitude1 = degrees2radians * coordinates1[1]; 417 | var bearing_rad = degrees2radians * bearing; 418 | 419 | var radians = distanceToRadians(distance, units); 420 | 421 | var latitude2 = Math.asin(Math.sin(latitude1) * Math.cos(radians) + 422 | Math.cos(latitude1) * Math.sin(radians) * Math.cos(bearing_rad)); 423 | var longitude2 = longitude1 + Math.atan2(Math.sin(bearing_rad) * 424 | Math.sin(radians) * Math.cos(latitude1), 425 | Math.cos(radians) - Math.sin(latitude1) * Math.sin(latitude2)); 426 | 427 | return point([radians2degrees * longitude2, radians2degrees * latitude2]); 428 | }; 429 | 430 | },{"@turf/helpers":7,"@turf/invariant":9}],6:[function(require,module,exports){ 431 | var getCoord = require('@turf/invariant').getCoord; 432 | var radiansToDistance = require('@turf/helpers').radiansToDistance; 433 | //http://en.wikipedia.org/wiki/Haversine_formula 434 | //http://www.movable-type.co.uk/scripts/latlong.html 435 | 436 | /** 437 | * Calculates the distance between two {@link Point|points} in degrees, radians, 438 | * miles, or kilometers. This uses the 439 | * [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula) 440 | * to account for global curvature. 441 | * 442 | * @name distance 443 | * @param {Feature} from origin point 444 | * @param {Feature} to destination point 445 | * @param {string} [units=kilometers] can be degrees, radians, miles, or kilometers 446 | * @return {number} distance between the two points 447 | * @example 448 | * var from = { 449 | * "type": "Feature", 450 | * "properties": {}, 451 | * "geometry": { 452 | * "type": "Point", 453 | * "coordinates": [-75.343, 39.984] 454 | * } 455 | * }; 456 | * var to = { 457 | * "type": "Feature", 458 | * "properties": {}, 459 | * "geometry": { 460 | * "type": "Point", 461 | * "coordinates": [-75.534, 39.123] 462 | * } 463 | * }; 464 | * var units = "miles"; 465 | * 466 | * var points = { 467 | * "type": "FeatureCollection", 468 | * "features": [from, to] 469 | * }; 470 | * 471 | * //=points 472 | * 473 | * var distance = turf.distance(from, to, units); 474 | * 475 | * //=distance 476 | */ 477 | module.exports = function (from, to, units) { 478 | var degrees2radians = Math.PI / 180; 479 | var coordinates1 = getCoord(from); 480 | var coordinates2 = getCoord(to); 481 | var dLat = degrees2radians * (coordinates2[1] - coordinates1[1]); 482 | var dLon = degrees2radians * (coordinates2[0] - coordinates1[0]); 483 | var lat1 = degrees2radians * coordinates1[1]; 484 | var lat2 = degrees2radians * coordinates2[1]; 485 | 486 | var a = Math.pow(Math.sin(dLat / 2), 2) + 487 | Math.pow(Math.sin(dLon / 2), 2) * Math.cos(lat1) * Math.cos(lat2); 488 | 489 | return radiansToDistance(2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)), units); 490 | }; 491 | 492 | },{"@turf/helpers":7,"@turf/invariant":9}],7:[function(require,module,exports){ 493 | /** 494 | * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}. 495 | * 496 | * @name feature 497 | * @param {Geometry} geometry input geometry 498 | * @param {Object} properties properties 499 | * @returns {FeatureCollection} a FeatureCollection of input features 500 | * @example 501 | * var geometry = { 502 | * "type": "Point", 503 | * "coordinates": [ 504 | * 67.5, 505 | * 32.84267363195431 506 | * ] 507 | * } 508 | * 509 | * var feature = turf.feature(geometry); 510 | * 511 | * //=feature 512 | */ 513 | function feature(geometry, properties) { 514 | return { 515 | type: 'Feature', 516 | properties: properties || {}, 517 | geometry: geometry 518 | }; 519 | } 520 | 521 | module.exports.feature = feature; 522 | 523 | /** 524 | * Takes coordinates and properties (optional) and returns a new {@link Point} feature. 525 | * 526 | * @name point 527 | * @param {Array} coordinates longitude, latitude position (each in decimal degrees) 528 | * @param {Object=} properties an Object that is used as the {@link Feature}'s 529 | * properties 530 | * @returns {Feature} a Point feature 531 | * @example 532 | * var pt1 = turf.point([-75.343, 39.984]); 533 | * 534 | * //=pt1 535 | */ 536 | module.exports.point = function (coordinates, properties) { 537 | if (!Array.isArray(coordinates)) throw new Error('Coordinates must be an array'); 538 | if (coordinates.length < 2) throw new Error('Coordinates must be at least 2 numbers long'); 539 | return feature({ 540 | type: 'Point', 541 | coordinates: coordinates.slice() 542 | }, properties); 543 | }; 544 | 545 | /** 546 | * Takes an array of LinearRings and optionally an {@link Object} with properties and returns a {@link Polygon} feature. 547 | * 548 | * @name polygon 549 | * @param {Array>>} coordinates an array of LinearRings 550 | * @param {Object=} properties a properties object 551 | * @returns {Feature} a Polygon feature 552 | * @throws {Error} throw an error if a LinearRing of the polygon has too few positions 553 | * or if a LinearRing of the Polygon does not have matching Positions at the 554 | * beginning & end. 555 | * @example 556 | * var polygon = turf.polygon([[ 557 | * [-2.275543, 53.464547], 558 | * [-2.275543, 53.489271], 559 | * [-2.215118, 53.489271], 560 | * [-2.215118, 53.464547], 561 | * [-2.275543, 53.464547] 562 | * ]], { name: 'poly1', population: 400}); 563 | * 564 | * //=polygon 565 | */ 566 | module.exports.polygon = function (coordinates, properties) { 567 | 568 | if (!coordinates) throw new Error('No coordinates passed'); 569 | 570 | for (var i = 0; i < coordinates.length; i++) { 571 | var ring = coordinates[i]; 572 | if (ring.length < 4) { 573 | throw new Error('Each LinearRing of a Polygon must have 4 or more Positions.'); 574 | } 575 | for (var j = 0; j < ring[ring.length - 1].length; j++) { 576 | if (ring[ring.length - 1][j] !== ring[0][j]) { 577 | throw new Error('First and last Position are not equivalent.'); 578 | } 579 | } 580 | } 581 | 582 | return feature({ 583 | type: 'Polygon', 584 | coordinates: coordinates 585 | }, properties); 586 | }; 587 | 588 | /** 589 | * Creates a {@link LineString} based on a 590 | * coordinate array. Properties can be added optionally. 591 | * 592 | * @name lineString 593 | * @param {Array>} coordinates an array of Positions 594 | * @param {Object=} properties an Object of key-value pairs to add as properties 595 | * @returns {Feature} a LineString feature 596 | * @throws {Error} if no coordinates are passed 597 | * @example 598 | * var linestring1 = turf.lineString([ 599 | * [-21.964416, 64.148203], 600 | * [-21.956176, 64.141316], 601 | * [-21.93901, 64.135924], 602 | * [-21.927337, 64.136673] 603 | * ]); 604 | * var linestring2 = turf.lineString([ 605 | * [-21.929054, 64.127985], 606 | * [-21.912918, 64.134726], 607 | * [-21.916007, 64.141016], 608 | * [-21.930084, 64.14446] 609 | * ], {name: 'line 1', distance: 145}); 610 | * 611 | * //=linestring1 612 | * 613 | * //=linestring2 614 | */ 615 | module.exports.lineString = function (coordinates, properties) { 616 | if (!coordinates) { 617 | throw new Error('No coordinates passed'); 618 | } 619 | return feature({ 620 | type: 'LineString', 621 | coordinates: coordinates 622 | }, properties); 623 | }; 624 | 625 | /** 626 | * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}. 627 | * 628 | * @name featureCollection 629 | * @param {Feature[]} features input features 630 | * @returns {FeatureCollection} a FeatureCollection of input features 631 | * @example 632 | * var features = [ 633 | * turf.point([-75.343, 39.984], {name: 'Location A'}), 634 | * turf.point([-75.833, 39.284], {name: 'Location B'}), 635 | * turf.point([-75.534, 39.123], {name: 'Location C'}) 636 | * ]; 637 | * 638 | * var fc = turf.featureCollection(features); 639 | * 640 | * //=fc 641 | */ 642 | module.exports.featureCollection = function (features) { 643 | return { 644 | type: 'FeatureCollection', 645 | features: features 646 | }; 647 | }; 648 | 649 | /** 650 | * Creates a {@link Feature} based on a 651 | * coordinate array. Properties can be added optionally. 652 | * 653 | * @name multiLineString 654 | * @param {Array>>} coordinates an array of LineStrings 655 | * @param {Object=} properties an Object of key-value pairs to add as properties 656 | * @returns {Feature} a MultiLineString feature 657 | * @throws {Error} if no coordinates are passed 658 | * @example 659 | * var multiLine = turf.multiLineString([[[0,0],[10,10]]]); 660 | * 661 | * //=multiLine 662 | * 663 | */ 664 | module.exports.multiLineString = function (coordinates, properties) { 665 | if (!coordinates) { 666 | throw new Error('No coordinates passed'); 667 | } 668 | return feature({ 669 | type: 'MultiLineString', 670 | coordinates: coordinates 671 | }, properties); 672 | }; 673 | 674 | /** 675 | * Creates a {@link Feature} based on a 676 | * coordinate array. Properties can be added optionally. 677 | * 678 | * @name multiPoint 679 | * @param {Array>} coordinates an array of Positions 680 | * @param {Object=} properties an Object of key-value pairs to add as properties 681 | * @returns {Feature} a MultiPoint feature 682 | * @throws {Error} if no coordinates are passed 683 | * @example 684 | * var multiPt = turf.multiPoint([[0,0],[10,10]]); 685 | * 686 | * //=multiPt 687 | * 688 | */ 689 | module.exports.multiPoint = function (coordinates, properties) { 690 | if (!coordinates) { 691 | throw new Error('No coordinates passed'); 692 | } 693 | return feature({ 694 | type: 'MultiPoint', 695 | coordinates: coordinates 696 | }, properties); 697 | }; 698 | 699 | 700 | /** 701 | * Creates a {@link Feature} based on a 702 | * coordinate array. Properties can be added optionally. 703 | * 704 | * @name multiPolygon 705 | * @param {Array>>>} coordinates an array of Polygons 706 | * @param {Object=} properties an Object of key-value pairs to add as properties 707 | * @returns {Feature} a multipolygon feature 708 | * @throws {Error} if no coordinates are passed 709 | * @example 710 | * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]]); 711 | * 712 | * //=multiPoly 713 | * 714 | */ 715 | module.exports.multiPolygon = function (coordinates, properties) { 716 | if (!coordinates) { 717 | throw new Error('No coordinates passed'); 718 | } 719 | return feature({ 720 | type: 'MultiPolygon', 721 | coordinates: coordinates 722 | }, properties); 723 | }; 724 | 725 | /** 726 | * Creates a {@link Feature} based on a 727 | * coordinate array. Properties can be added optionally. 728 | * 729 | * @name geometryCollection 730 | * @param {Array<{Geometry}>} geometries an array of GeoJSON Geometries 731 | * @param {Object=} properties an Object of key-value pairs to add as properties 732 | * @returns {Feature} a GeoJSON GeometryCollection Feature 733 | * @example 734 | * var pt = { 735 | * "type": "Point", 736 | * "coordinates": [100, 0] 737 | * }; 738 | * var line = { 739 | * "type": "LineString", 740 | * "coordinates": [ [101, 0], [102, 1] ] 741 | * }; 742 | * var collection = turf.geometryCollection([pt, line]); 743 | * 744 | * //=collection 745 | */ 746 | module.exports.geometryCollection = function (geometries, properties) { 747 | return feature({ 748 | type: 'GeometryCollection', 749 | geometries: geometries 750 | }, properties); 751 | }; 752 | 753 | var factors = { 754 | miles: 3960, 755 | nauticalmiles: 3441.145, 756 | degrees: 57.2957795, 757 | radians: 1, 758 | inches: 250905600, 759 | yards: 6969600, 760 | meters: 6373000, 761 | metres: 6373000, 762 | kilometers: 6373, 763 | kilometres: 6373, 764 | feet: 20908792.65 765 | }; 766 | 767 | /* 768 | * Convert a distance measurement from radians to a more friendly unit. 769 | * 770 | * @name radiansToDistance 771 | * @param {number} distance in radians across the sphere 772 | * @param {string} [units=kilometers] can be degrees, radians, miles, or kilometers 773 | * inches, yards, metres, meters, kilometres, kilometers. 774 | * @returns {number} distance 775 | */ 776 | module.exports.radiansToDistance = function (radians, units) { 777 | var factor = factors[units || 'kilometers']; 778 | if (factor === undefined) { 779 | throw new Error('Invalid unit'); 780 | } 781 | return radians * factor; 782 | }; 783 | 784 | /* 785 | * Convert a distance measurement from a real-world unit into radians 786 | * 787 | * @name distanceToRadians 788 | * @param {number} distance in real units 789 | * @param {string} [units=kilometers] can be degrees, radians, miles, or kilometers 790 | * inches, yards, metres, meters, kilometres, kilometers. 791 | * @returns {number} radians 792 | */ 793 | module.exports.distanceToRadians = function (distance, units) { 794 | var factor = factors[units || 'kilometers']; 795 | if (factor === undefined) { 796 | throw new Error('Invalid unit'); 797 | } 798 | return distance / factor; 799 | }; 800 | 801 | /* 802 | * Convert a distance measurement from a real-world unit into degrees 803 | * 804 | * @name distanceToRadians 805 | * @param {number} distance in real units 806 | * @param {string} [units=kilometers] can be degrees, radians, miles, or kilometers 807 | * inches, yards, metres, meters, kilometres, kilometers. 808 | * @returns {number} degrees 809 | */ 810 | module.exports.distanceToDegrees = function (distance, units) { 811 | var factor = factors[units || 'kilometers']; 812 | if (factor === undefined) { 813 | throw new Error('Invalid unit'); 814 | } 815 | return (distance / factor) * 57.2958; 816 | }; 817 | 818 | },{}],8:[function(require,module,exports){ 819 | var point = require('@turf/helpers').point; 820 | var polygon = require('@turf/helpers').polygon; 821 | var distance = require('@turf/distance'); 822 | var featurecollection = require('@turf/helpers').featureCollection; 823 | 824 | //Precompute cosines and sines of angles used in hexagon creation 825 | // for performance gain 826 | var cosines = []; 827 | var sines = []; 828 | for (var i = 0; i < 6; i++) { 829 | var angle = 2 * Math.PI / 6 * i; 830 | cosines.push(Math.cos(angle)); 831 | sines.push(Math.sin(angle)); 832 | } 833 | 834 | /** 835 | * Takes a bounding box and a cell size in degrees and returns a {@link FeatureCollection} of flat-topped 836 | * hexagons ({@link Polygon} features) aligned in an "odd-q" vertical grid as 837 | * described in [Hexagonal Grids](http://www.redblobgames.com/grids/hexagons/). 838 | * 839 | * @name hexGrid 840 | * @param {Array} bbox extent in [minX, minY, maxX, maxY] order 841 | * @param {number} cellSize dimension of cell in specified units 842 | * @param {string} [units=kilometers] used in calculating cellSize, can be degrees, radians, miles, or kilometers 843 | * @param {boolean} [triangles=false] whether to return as triangles instead of hexagons 844 | * @return {FeatureCollection} a hexagonal grid 845 | * @example 846 | * var bbox = [-96,31,-84,40]; 847 | * var cellSize = 50; 848 | * var units = 'miles'; 849 | * 850 | * var hexgrid = turf.hexGrid(bbox, cellSize, units); 851 | * 852 | * //=hexgrid 853 | */ 854 | module.exports = function hexGrid(bbox, cellSize, units, triangles) { 855 | var xFraction = cellSize / (distance(point([bbox[0], bbox[1]]), point([bbox[2], bbox[1]]), units)); 856 | var cellWidth = xFraction * (bbox[2] - bbox[0]); 857 | var yFraction = cellSize / (distance(point([bbox[0], bbox[1]]), point([bbox[0], bbox[3]]), units)); 858 | var cellHeight = yFraction * (bbox[3] - bbox[1]); 859 | var radius = cellWidth / 2; 860 | 861 | var hex_width = radius * 2; 862 | var hex_height = Math.sqrt(3) / 2 * cellHeight; 863 | 864 | var box_width = bbox[2] - bbox[0]; 865 | var box_height = bbox[3] - bbox[1]; 866 | 867 | var x_interval = 3 / 4 * hex_width; 868 | var y_interval = hex_height; 869 | 870 | var x_span = box_width / (hex_width - radius / 2); 871 | var x_count = Math.ceil(x_span); 872 | if (Math.round(x_span) === x_count) { 873 | x_count++; 874 | } 875 | 876 | var x_adjust = ((x_count * x_interval - radius / 2) - box_width) / 2 - radius / 2; 877 | 878 | var y_count = Math.ceil(box_height / hex_height); 879 | 880 | var y_adjust = (box_height - y_count * hex_height) / 2; 881 | 882 | var hasOffsetY = y_count * hex_height - box_height > hex_height / 2; 883 | if (hasOffsetY) { 884 | y_adjust -= hex_height / 4; 885 | } 886 | 887 | var fc = featurecollection([]); 888 | for (var x = 0; x < x_count; x++) { 889 | for (var y = 0; y <= y_count; y++) { 890 | 891 | var isOdd = x % 2 === 1; 892 | if (y === 0 && isOdd) { 893 | continue; 894 | } 895 | 896 | if (y === 0 && hasOffsetY) { 897 | continue; 898 | } 899 | 900 | var center_x = x * x_interval + bbox[0] - x_adjust; 901 | var center_y = y * y_interval + bbox[1] + y_adjust; 902 | 903 | if (isOdd) { 904 | center_y -= hex_height / 2; 905 | } 906 | if (triangles) { 907 | fc.features.push.apply(fc.features, hexTriangles([center_x, center_y], cellWidth / 2, cellHeight / 2)); 908 | } else { 909 | fc.features.push(hexagon([center_x, center_y], cellWidth / 2, cellHeight / 2)); 910 | } 911 | } 912 | } 913 | 914 | return fc; 915 | }; 916 | 917 | //Center should be [x, y] 918 | function hexagon(center, rx, ry) { 919 | var vertices = []; 920 | for (var i = 0; i < 6; i++) { 921 | var x = center[0] + rx * cosines[i]; 922 | var y = center[1] + ry * sines[i]; 923 | vertices.push([x, y]); 924 | } 925 | //first and last vertex must be the same 926 | vertices.push(vertices[0]); 927 | return polygon([vertices]); 928 | } 929 | 930 | //Center should be [x, y] 931 | function hexTriangles(center, rx, ry) { 932 | var triangles = []; 933 | for (var i = 0; i < 6; i++) { 934 | var vertices = []; 935 | vertices.push(center); 936 | vertices.push([ 937 | center[0] + rx * cosines[i], 938 | center[1] + ry * sines[i] 939 | ]); 940 | vertices.push([ 941 | center[0] + rx * cosines[(i + 1) % 6], 942 | center[1] + ry * sines[(i + 1) % 6] 943 | ]); 944 | vertices.push(center); 945 | triangles.push(polygon([vertices])); 946 | } 947 | return triangles; 948 | } 949 | 950 | },{"@turf/distance":6,"@turf/helpers":7}],9:[function(require,module,exports){ 951 | /** 952 | * Unwrap a coordinate from a Feature with a Point geometry, a Point 953 | * geometry, or a single coordinate. 954 | * 955 | * @param {*} obj any value 956 | * @returns {Array} a coordinate 957 | */ 958 | function getCoord(obj) { 959 | if (Array.isArray(obj) && 960 | typeof obj[0] === 'number' && 961 | typeof obj[1] === 'number') { 962 | return obj; 963 | } else if (obj) { 964 | if (obj.type === 'Feature' && 965 | obj.geometry && 966 | obj.geometry.type === 'Point' && 967 | Array.isArray(obj.geometry.coordinates)) { 968 | return obj.geometry.coordinates; 969 | } else if (obj.type === 'Point' && 970 | Array.isArray(obj.coordinates)) { 971 | return obj.coordinates; 972 | } 973 | } 974 | throw new Error('A coordinate, feature, or point geometry is required'); 975 | } 976 | 977 | /** 978 | * Enforce expectations about types of GeoJSON objects for Turf. 979 | * 980 | * @alias geojsonType 981 | * @param {GeoJSON} value any GeoJSON object 982 | * @param {string} type expected GeoJSON type 983 | * @param {string} name name of calling function 984 | * @throws {Error} if value is not the expected type. 985 | */ 986 | function geojsonType(value, type, name) { 987 | if (!type || !name) throw new Error('type and name required'); 988 | 989 | if (!value || value.type !== type) { 990 | throw new Error('Invalid input to ' + name + ': must be a ' + type + ', given ' + value.type); 991 | } 992 | } 993 | 994 | /** 995 | * Enforce expectations about types of {@link Feature} inputs for Turf. 996 | * Internally this uses {@link geojsonType} to judge geometry types. 997 | * 998 | * @alias featureOf 999 | * @param {Feature} feature a feature with an expected geometry type 1000 | * @param {string} type expected GeoJSON type 1001 | * @param {string} name name of calling function 1002 | * @throws {Error} error if value is not the expected type. 1003 | */ 1004 | function featureOf(feature, type, name) { 1005 | if (!name) throw new Error('.featureOf() requires a name'); 1006 | if (!feature || feature.type !== 'Feature' || !feature.geometry) { 1007 | throw new Error('Invalid input to ' + name + ', Feature with geometry required'); 1008 | } 1009 | if (!feature.geometry || feature.geometry.type !== type) { 1010 | throw new Error('Invalid input to ' + name + ': must be a ' + type + ', given ' + feature.geometry.type); 1011 | } 1012 | } 1013 | 1014 | /** 1015 | * Enforce expectations about types of {@link FeatureCollection} inputs for Turf. 1016 | * Internally this uses {@link geojsonType} to judge geometry types. 1017 | * 1018 | * @alias collectionOf 1019 | * @param {FeatureCollection} featurecollection a featurecollection for which features will be judged 1020 | * @param {string} type expected GeoJSON type 1021 | * @param {string} name name of calling function 1022 | * @throws {Error} if value is not the expected type. 1023 | */ 1024 | function collectionOf(featurecollection, type, name) { 1025 | if (!name) throw new Error('.collectionOf() requires a name'); 1026 | if (!featurecollection || featurecollection.type !== 'FeatureCollection') { 1027 | throw new Error('Invalid input to ' + name + ', FeatureCollection required'); 1028 | } 1029 | for (var i = 0; i < featurecollection.features.length; i++) { 1030 | var feature = featurecollection.features[i]; 1031 | if (!feature || feature.type !== 'Feature' || !feature.geometry) { 1032 | throw new Error('Invalid input to ' + name + ', Feature with geometry required'); 1033 | } 1034 | if (!feature.geometry || feature.geometry.type !== type) { 1035 | throw new Error('Invalid input to ' + name + ': must be a ' + type + ', given ' + feature.geometry.type); 1036 | } 1037 | } 1038 | } 1039 | 1040 | module.exports.geojsonType = geojsonType; 1041 | module.exports.collectionOf = collectionOf; 1042 | module.exports.featureOf = featureOf; 1043 | module.exports.getCoord = getCoord; 1044 | 1045 | },{}],10:[function(require,module,exports){ 1046 | /** 1047 | * Iterate over coordinates in any GeoJSON object, similar to 1048 | * Array.forEach. 1049 | * 1050 | * @name coordEach 1051 | * @param {Object} layer any GeoJSON object 1052 | * @param {Function} callback a method that takes (value) 1053 | * @param {boolean=} excludeWrapCoord whether or not to include 1054 | * the final coordinate of LinearRings that wraps the ring in its iteration. 1055 | * @example 1056 | * var point = { type: 'Point', coordinates: [0, 0] }; 1057 | * turfMeta.coordEach(point, function(coords) { 1058 | * // coords is equal to [0, 0] 1059 | * }); 1060 | */ 1061 | function coordEach(layer, callback, excludeWrapCoord) { 1062 | var i, j, k, g, l, geometry, stopG, coords, 1063 | geometryMaybeCollection, 1064 | wrapShrink = 0, 1065 | isGeometryCollection, 1066 | isFeatureCollection = layer.type === 'FeatureCollection', 1067 | isFeature = layer.type === 'Feature', 1068 | stop = isFeatureCollection ? layer.features.length : 1; 1069 | 1070 | // This logic may look a little weird. The reason why it is that way 1071 | // is because it's trying to be fast. GeoJSON supports multiple kinds 1072 | // of objects at its root: FeatureCollection, Features, Geometries. 1073 | // This function has the responsibility of handling all of them, and that 1074 | // means that some of the `for` loops you see below actually just don't apply 1075 | // to certain inputs. For instance, if you give this just a 1076 | // Point geometry, then both loops are short-circuited and all we do 1077 | // is gradually rename the input until it's called 'geometry'. 1078 | // 1079 | // This also aims to allocate as few resources as possible: just a 1080 | // few numbers and booleans, rather than any temporary arrays as would 1081 | // be required with the normalization approach. 1082 | for (i = 0; i < stop; i++) { 1083 | 1084 | geometryMaybeCollection = (isFeatureCollection ? layer.features[i].geometry : 1085 | (isFeature ? layer.geometry : layer)); 1086 | isGeometryCollection = geometryMaybeCollection.type === 'GeometryCollection'; 1087 | stopG = isGeometryCollection ? geometryMaybeCollection.geometries.length : 1; 1088 | 1089 | for (g = 0; g < stopG; g++) { 1090 | geometry = isGeometryCollection ? 1091 | geometryMaybeCollection.geometries[g] : geometryMaybeCollection; 1092 | coords = geometry.coordinates; 1093 | 1094 | wrapShrink = (excludeWrapCoord && 1095 | (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon')) ? 1096 | 1 : 0; 1097 | 1098 | if (geometry.type === 'Point') { 1099 | callback(coords); 1100 | } else if (geometry.type === 'LineString' || geometry.type === 'MultiPoint') { 1101 | for (j = 0; j < coords.length; j++) callback(coords[j]); 1102 | } else if (geometry.type === 'Polygon' || geometry.type === 'MultiLineString') { 1103 | for (j = 0; j < coords.length; j++) 1104 | for (k = 0; k < coords[j].length - wrapShrink; k++) 1105 | callback(coords[j][k]); 1106 | } else if (geometry.type === 'MultiPolygon') { 1107 | for (j = 0; j < coords.length; j++) 1108 | for (k = 0; k < coords[j].length; k++) 1109 | for (l = 0; l < coords[j][k].length - wrapShrink; l++) 1110 | callback(coords[j][k][l]); 1111 | } else if (geometry.type === 'GeometryCollection') { 1112 | for (j = 0; j < geometry.geometries.length; j++) 1113 | coordEach(geometry.geometries[j], callback, excludeWrapCoord); 1114 | } else { 1115 | throw new Error('Unknown Geometry Type'); 1116 | } 1117 | } 1118 | } 1119 | } 1120 | module.exports.coordEach = coordEach; 1121 | 1122 | /** 1123 | * Reduce coordinates in any GeoJSON object into a single value, 1124 | * similar to how Array.reduce works. However, in this case we lazily run 1125 | * the reduction, so an array of all coordinates is unnecessary. 1126 | * 1127 | * @name coordReduce 1128 | * @param {Object} layer any GeoJSON object 1129 | * @param {Function} callback a method that takes (memo, value) and returns 1130 | * a new memo 1131 | * @param {*} memo the starting value of memo: can be any type. 1132 | * @param {boolean=} excludeWrapCoord whether or not to include 1133 | * the final coordinate of LinearRings that wraps the ring in its iteration. 1134 | * @returns {*} combined value 1135 | */ 1136 | function coordReduce(layer, callback, memo, excludeWrapCoord) { 1137 | coordEach(layer, function (coord) { 1138 | memo = callback(memo, coord); 1139 | }, excludeWrapCoord); 1140 | return memo; 1141 | } 1142 | module.exports.coordReduce = coordReduce; 1143 | 1144 | /** 1145 | * Iterate over property objects in any GeoJSON object, similar to 1146 | * Array.forEach. 1147 | * 1148 | * @name propEach 1149 | * @param {Object} layer any GeoJSON object 1150 | * @param {Function} callback a method that takes (value) 1151 | * @example 1152 | * var point = { type: 'Feature', geometry: null, properties: { foo: 1 } }; 1153 | * turfMeta.propEach(point, function(props) { 1154 | * // props is equal to { foo: 1} 1155 | * }); 1156 | */ 1157 | function propEach(layer, callback) { 1158 | var i; 1159 | switch (layer.type) { 1160 | case 'FeatureCollection': 1161 | for (i = 0; i < layer.features.length; i++) { 1162 | callback(layer.features[i].properties, i); 1163 | } 1164 | break; 1165 | case 'Feature': 1166 | callback(layer.properties, 0); 1167 | break; 1168 | } 1169 | } 1170 | module.exports.propEach = propEach; 1171 | 1172 | /** 1173 | * Reduce properties in any GeoJSON object into a single value, 1174 | * similar to how Array.reduce works. However, in this case we lazily run 1175 | * the reduction, so an array of all properties is unnecessary. 1176 | * 1177 | * @name propReduce 1178 | * @param {Object} layer any GeoJSON object 1179 | * @param {Function} callback a method that takes (memo, coord) and returns 1180 | * a new memo 1181 | * @param {*} memo the starting value of memo: can be any type. 1182 | * @returns {*} combined value 1183 | * @example 1184 | * // an example of an even more advanced function that gives you the 1185 | * // javascript type of each property of every feature 1186 | * function propTypes (layer) { 1187 | * opts = opts || {} 1188 | * return turfMeta.propReduce(layer, function (prev, props) { 1189 | * for (var prop in props) { 1190 | * if (prev[prop]) continue 1191 | * prev[prop] = typeof props[prop] 1192 | * } 1193 | * }, {}) 1194 | * } 1195 | */ 1196 | function propReduce(layer, callback, memo) { 1197 | propEach(layer, function (prop, i) { 1198 | memo = callback(memo, prop, i); 1199 | }); 1200 | return memo; 1201 | } 1202 | module.exports.propReduce = propReduce; 1203 | 1204 | /** 1205 | * Iterate over features in any GeoJSON object, similar to 1206 | * Array.forEach. 1207 | * 1208 | * @name featureEach 1209 | * @param {Object} layer any GeoJSON object 1210 | * @param {Function} callback a method that takes (value) 1211 | * @example 1212 | * var feature = { type: 'Feature', geometry: null, properties: {} }; 1213 | * turfMeta.featureEach(feature, function(feature) { 1214 | * // feature == feature 1215 | * }); 1216 | */ 1217 | function featureEach(layer, callback) { 1218 | if (layer.type === 'Feature') { 1219 | callback(layer, 0); 1220 | } else if (layer.type === 'FeatureCollection') { 1221 | for (var i = 0; i < layer.features.length; i++) { 1222 | callback(layer.features[i], i); 1223 | } 1224 | } 1225 | } 1226 | module.exports.featureEach = featureEach; 1227 | 1228 | /** 1229 | * Get all coordinates from any GeoJSON object, returning an array of coordinate 1230 | * arrays. 1231 | * 1232 | * @name coordAll 1233 | * @param {Object} layer any GeoJSON object 1234 | * @returns {Array>} coordinate position array 1235 | */ 1236 | function coordAll(layer) { 1237 | var coords = []; 1238 | coordEach(layer, function (coord) { 1239 | coords.push(coord); 1240 | }); 1241 | return coords; 1242 | } 1243 | module.exports.coordAll = coordAll; 1244 | 1245 | /** 1246 | * Iterate over each geometry in any GeoJSON object, similar to 1247 | * Array.forEach. 1248 | * 1249 | * @name geomEach 1250 | * @param {Object} layer any GeoJSON object 1251 | * @param {Function} callback a method that takes (value) 1252 | * @example 1253 | * var point = { 1254 | * type: 'Feature', 1255 | * geometry: { type: 'Point', coordinates: [0, 0] }, 1256 | * properties: {} 1257 | * }; 1258 | * turfMeta.geomEach(point, function(geom) { 1259 | * // geom is the point geometry 1260 | * }); 1261 | */ 1262 | function geomEach(layer, callback) { 1263 | var i, j, g, geometry, stopG, 1264 | geometryMaybeCollection, 1265 | isGeometryCollection, 1266 | isFeatureCollection = layer.type === 'FeatureCollection', 1267 | isFeature = layer.type === 'Feature', 1268 | stop = isFeatureCollection ? layer.features.length : 1; 1269 | 1270 | // This logic may look a little weird. The reason why it is that way 1271 | // is because it's trying to be fast. GeoJSON supports multiple kinds 1272 | // of objects at its root: FeatureCollection, Features, Geometries. 1273 | // This function has the responsibility of handling all of them, and that 1274 | // means that some of the `for` loops you see below actually just don't apply 1275 | // to certain inputs. For instance, if you give this just a 1276 | // Point geometry, then both loops are short-circuited and all we do 1277 | // is gradually rename the input until it's called 'geometry'. 1278 | // 1279 | // This also aims to allocate as few resources as possible: just a 1280 | // few numbers and booleans, rather than any temporary arrays as would 1281 | // be required with the normalization approach. 1282 | for (i = 0; i < stop; i++) { 1283 | 1284 | geometryMaybeCollection = (isFeatureCollection ? layer.features[i].geometry : 1285 | (isFeature ? layer.geometry : layer)); 1286 | isGeometryCollection = geometryMaybeCollection.type === 'GeometryCollection'; 1287 | stopG = isGeometryCollection ? geometryMaybeCollection.geometries.length : 1; 1288 | 1289 | for (g = 0; g < stopG; g++) { 1290 | geometry = isGeometryCollection ? 1291 | geometryMaybeCollection.geometries[g] : geometryMaybeCollection; 1292 | 1293 | if (geometry.type === 'Point' || 1294 | geometry.type === 'LineString' || 1295 | geometry.type === 'MultiPoint' || 1296 | geometry.type === 'Polygon' || 1297 | geometry.type === 'MultiLineString' || 1298 | geometry.type === 'MultiPolygon') { 1299 | callback(geometry); 1300 | } else if (geometry.type === 'GeometryCollection') { 1301 | for (j = 0; j < geometry.geometries.length; j++) 1302 | callback(geometry.geometries[j]); 1303 | } else { 1304 | throw new Error('Unknown Geometry Type'); 1305 | } 1306 | } 1307 | } 1308 | } 1309 | module.exports.geomEach = geomEach; 1310 | 1311 | },{}],11:[function(require,module,exports){ 1312 | 'use strict'; 1313 | 1314 | module.exports = partialSort; 1315 | 1316 | // Floyd-Rivest selection algorithm: 1317 | // Rearrange items so that all items in the [left, k] range are smaller than all items in (k, right]; 1318 | // The k-th element will have the (k - left + 1)th smallest value in [left, right] 1319 | 1320 | function partialSort(arr, k, left, right, compare) { 1321 | left = left || 0; 1322 | right = right || (arr.length - 1); 1323 | compare = compare || defaultCompare; 1324 | 1325 | while (right > left) { 1326 | if (right - left > 600) { 1327 | var n = right - left + 1; 1328 | var m = k - left + 1; 1329 | var z = Math.log(n); 1330 | var s = 0.5 * Math.exp(2 * z / 3); 1331 | var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); 1332 | var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); 1333 | var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); 1334 | partialSort(arr, k, newLeft, newRight, compare); 1335 | } 1336 | 1337 | var t = arr[k]; 1338 | var i = left; 1339 | var j = right; 1340 | 1341 | swap(arr, left, k); 1342 | if (compare(arr[right], t) > 0) swap(arr, left, right); 1343 | 1344 | while (i < j) { 1345 | swap(arr, i, j); 1346 | i++; 1347 | j--; 1348 | while (compare(arr[i], t) < 0) i++; 1349 | while (compare(arr[j], t) > 0) j--; 1350 | } 1351 | 1352 | if (compare(arr[left], t) === 0) swap(arr, left, j); 1353 | else { 1354 | j++; 1355 | swap(arr, j, right); 1356 | } 1357 | 1358 | if (j <= k) left = j + 1; 1359 | if (k <= j) right = j - 1; 1360 | } 1361 | } 1362 | 1363 | function swap(arr, i, j) { 1364 | var tmp = arr[i]; 1365 | arr[i] = arr[j]; 1366 | arr[j] = tmp; 1367 | } 1368 | 1369 | function defaultCompare(a, b) { 1370 | return a < b ? -1 : a > b ? 1 : 0; 1371 | } 1372 | 1373 | },{}],12:[function(require,module,exports){ 1374 | 'use strict'; 1375 | 1376 | module.exports = rbush; 1377 | 1378 | var quickselect = require('quickselect'); 1379 | 1380 | function rbush(maxEntries, format) { 1381 | if (!(this instanceof rbush)) return new rbush(maxEntries, format); 1382 | 1383 | // max entries in a node is 9 by default; min node fill is 40% for best performance 1384 | this._maxEntries = Math.max(4, maxEntries || 9); 1385 | this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)); 1386 | 1387 | if (format) { 1388 | this._initFormat(format); 1389 | } 1390 | 1391 | this.clear(); 1392 | } 1393 | 1394 | rbush.prototype = { 1395 | 1396 | all: function () { 1397 | return this._all(this.data, []); 1398 | }, 1399 | 1400 | search: function (bbox) { 1401 | 1402 | var node = this.data, 1403 | result = [], 1404 | toBBox = this.toBBox; 1405 | 1406 | if (!intersects(bbox, node)) return result; 1407 | 1408 | var nodesToSearch = [], 1409 | i, len, child, childBBox; 1410 | 1411 | while (node) { 1412 | for (i = 0, len = node.children.length; i < len; i++) { 1413 | 1414 | child = node.children[i]; 1415 | childBBox = node.leaf ? toBBox(child) : child; 1416 | 1417 | if (intersects(bbox, childBBox)) { 1418 | if (node.leaf) result.push(child); 1419 | else if (contains(bbox, childBBox)) this._all(child, result); 1420 | else nodesToSearch.push(child); 1421 | } 1422 | } 1423 | node = nodesToSearch.pop(); 1424 | } 1425 | 1426 | return result; 1427 | }, 1428 | 1429 | collides: function (bbox) { 1430 | 1431 | var node = this.data, 1432 | toBBox = this.toBBox; 1433 | 1434 | if (!intersects(bbox, node)) return false; 1435 | 1436 | var nodesToSearch = [], 1437 | i, len, child, childBBox; 1438 | 1439 | while (node) { 1440 | for (i = 0, len = node.children.length; i < len; i++) { 1441 | 1442 | child = node.children[i]; 1443 | childBBox = node.leaf ? toBBox(child) : child; 1444 | 1445 | if (intersects(bbox, childBBox)) { 1446 | if (node.leaf || contains(bbox, childBBox)) return true; 1447 | nodesToSearch.push(child); 1448 | } 1449 | } 1450 | node = nodesToSearch.pop(); 1451 | } 1452 | 1453 | return false; 1454 | }, 1455 | 1456 | load: function (data) { 1457 | if (!(data && data.length)) return this; 1458 | 1459 | if (data.length < this._minEntries) { 1460 | for (var i = 0, len = data.length; i < len; i++) { 1461 | this.insert(data[i]); 1462 | } 1463 | return this; 1464 | } 1465 | 1466 | // recursively build the tree with the given data from stratch using OMT algorithm 1467 | var node = this._build(data.slice(), 0, data.length - 1, 0); 1468 | 1469 | if (!this.data.children.length) { 1470 | // save as is if tree is empty 1471 | this.data = node; 1472 | 1473 | } else if (this.data.height === node.height) { 1474 | // split root if trees have the same height 1475 | this._splitRoot(this.data, node); 1476 | 1477 | } else { 1478 | if (this.data.height < node.height) { 1479 | // swap trees if inserted one is bigger 1480 | var tmpNode = this.data; 1481 | this.data = node; 1482 | node = tmpNode; 1483 | } 1484 | 1485 | // insert the small tree into the large tree at appropriate level 1486 | this._insert(node, this.data.height - node.height - 1, true); 1487 | } 1488 | 1489 | return this; 1490 | }, 1491 | 1492 | insert: function (item) { 1493 | if (item) this._insert(item, this.data.height - 1); 1494 | return this; 1495 | }, 1496 | 1497 | clear: function () { 1498 | this.data = createNode([]); 1499 | return this; 1500 | }, 1501 | 1502 | remove: function (item, equalsFn) { 1503 | if (!item) return this; 1504 | 1505 | var node = this.data, 1506 | bbox = this.toBBox(item), 1507 | path = [], 1508 | indexes = [], 1509 | i, parent, index, goingUp; 1510 | 1511 | // depth-first iterative tree traversal 1512 | while (node || path.length) { 1513 | 1514 | if (!node) { // go up 1515 | node = path.pop(); 1516 | parent = path[path.length - 1]; 1517 | i = indexes.pop(); 1518 | goingUp = true; 1519 | } 1520 | 1521 | if (node.leaf) { // check current node 1522 | index = findItem(item, node.children, equalsFn); 1523 | 1524 | if (index !== -1) { 1525 | // item found, remove the item and condense tree upwards 1526 | node.children.splice(index, 1); 1527 | path.push(node); 1528 | this._condense(path); 1529 | return this; 1530 | } 1531 | } 1532 | 1533 | if (!goingUp && !node.leaf && contains(node, bbox)) { // go down 1534 | path.push(node); 1535 | indexes.push(i); 1536 | i = 0; 1537 | parent = node; 1538 | node = node.children[0]; 1539 | 1540 | } else if (parent) { // go right 1541 | i++; 1542 | node = parent.children[i]; 1543 | goingUp = false; 1544 | 1545 | } else node = null; // nothing found 1546 | } 1547 | 1548 | return this; 1549 | }, 1550 | 1551 | toBBox: function (item) { return item; }, 1552 | 1553 | compareMinX: compareNodeMinX, 1554 | compareMinY: compareNodeMinY, 1555 | 1556 | toJSON: function () { return this.data; }, 1557 | 1558 | fromJSON: function (data) { 1559 | this.data = data; 1560 | return this; 1561 | }, 1562 | 1563 | _all: function (node, result) { 1564 | var nodesToSearch = []; 1565 | while (node) { 1566 | if (node.leaf) result.push.apply(result, node.children); 1567 | else nodesToSearch.push.apply(nodesToSearch, node.children); 1568 | 1569 | node = nodesToSearch.pop(); 1570 | } 1571 | return result; 1572 | }, 1573 | 1574 | _build: function (items, left, right, height) { 1575 | 1576 | var N = right - left + 1, 1577 | M = this._maxEntries, 1578 | node; 1579 | 1580 | if (N <= M) { 1581 | // reached leaf level; return leaf 1582 | node = createNode(items.slice(left, right + 1)); 1583 | calcBBox(node, this.toBBox); 1584 | return node; 1585 | } 1586 | 1587 | if (!height) { 1588 | // target height of the bulk-loaded tree 1589 | height = Math.ceil(Math.log(N) / Math.log(M)); 1590 | 1591 | // target number of root entries to maximize storage utilization 1592 | M = Math.ceil(N / Math.pow(M, height - 1)); 1593 | } 1594 | 1595 | node = createNode([]); 1596 | node.leaf = false; 1597 | node.height = height; 1598 | 1599 | // split the items into M mostly square tiles 1600 | 1601 | var N2 = Math.ceil(N / M), 1602 | N1 = N2 * Math.ceil(Math.sqrt(M)), 1603 | i, j, right2, right3; 1604 | 1605 | multiSelect(items, left, right, N1, this.compareMinX); 1606 | 1607 | for (i = left; i <= right; i += N1) { 1608 | 1609 | right2 = Math.min(i + N1 - 1, right); 1610 | 1611 | multiSelect(items, i, right2, N2, this.compareMinY); 1612 | 1613 | for (j = i; j <= right2; j += N2) { 1614 | 1615 | right3 = Math.min(j + N2 - 1, right2); 1616 | 1617 | // pack each entry recursively 1618 | node.children.push(this._build(items, j, right3, height - 1)); 1619 | } 1620 | } 1621 | 1622 | calcBBox(node, this.toBBox); 1623 | 1624 | return node; 1625 | }, 1626 | 1627 | _chooseSubtree: function (bbox, node, level, path) { 1628 | 1629 | var i, len, child, targetNode, area, enlargement, minArea, minEnlargement; 1630 | 1631 | while (true) { 1632 | path.push(node); 1633 | 1634 | if (node.leaf || path.length - 1 === level) break; 1635 | 1636 | minArea = minEnlargement = Infinity; 1637 | 1638 | for (i = 0, len = node.children.length; i < len; i++) { 1639 | child = node.children[i]; 1640 | area = bboxArea(child); 1641 | enlargement = enlargedArea(bbox, child) - area; 1642 | 1643 | // choose entry with the least area enlargement 1644 | if (enlargement < minEnlargement) { 1645 | minEnlargement = enlargement; 1646 | minArea = area < minArea ? area : minArea; 1647 | targetNode = child; 1648 | 1649 | } else if (enlargement === minEnlargement) { 1650 | // otherwise choose one with the smallest area 1651 | if (area < minArea) { 1652 | minArea = area; 1653 | targetNode = child; 1654 | } 1655 | } 1656 | } 1657 | 1658 | node = targetNode || node.children[0]; 1659 | } 1660 | 1661 | return node; 1662 | }, 1663 | 1664 | _insert: function (item, level, isNode) { 1665 | 1666 | var toBBox = this.toBBox, 1667 | bbox = isNode ? item : toBBox(item), 1668 | insertPath = []; 1669 | 1670 | // find the best node for accommodating the item, saving all nodes along the path too 1671 | var node = this._chooseSubtree(bbox, this.data, level, insertPath); 1672 | 1673 | // put the item into the node 1674 | node.children.push(item); 1675 | extend(node, bbox); 1676 | 1677 | // split on node overflow; propagate upwards if necessary 1678 | while (level >= 0) { 1679 | if (insertPath[level].children.length > this._maxEntries) { 1680 | this._split(insertPath, level); 1681 | level--; 1682 | } else break; 1683 | } 1684 | 1685 | // adjust bboxes along the insertion path 1686 | this._adjustParentBBoxes(bbox, insertPath, level); 1687 | }, 1688 | 1689 | // split overflowed node into two 1690 | _split: function (insertPath, level) { 1691 | 1692 | var node = insertPath[level], 1693 | M = node.children.length, 1694 | m = this._minEntries; 1695 | 1696 | this._chooseSplitAxis(node, m, M); 1697 | 1698 | var splitIndex = this._chooseSplitIndex(node, m, M); 1699 | 1700 | var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)); 1701 | newNode.height = node.height; 1702 | newNode.leaf = node.leaf; 1703 | 1704 | calcBBox(node, this.toBBox); 1705 | calcBBox(newNode, this.toBBox); 1706 | 1707 | if (level) insertPath[level - 1].children.push(newNode); 1708 | else this._splitRoot(node, newNode); 1709 | }, 1710 | 1711 | _splitRoot: function (node, newNode) { 1712 | // split root node 1713 | this.data = createNode([node, newNode]); 1714 | this.data.height = node.height + 1; 1715 | this.data.leaf = false; 1716 | calcBBox(this.data, this.toBBox); 1717 | }, 1718 | 1719 | _chooseSplitIndex: function (node, m, M) { 1720 | 1721 | var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index; 1722 | 1723 | minOverlap = minArea = Infinity; 1724 | 1725 | for (i = m; i <= M - m; i++) { 1726 | bbox1 = distBBox(node, 0, i, this.toBBox); 1727 | bbox2 = distBBox(node, i, M, this.toBBox); 1728 | 1729 | overlap = intersectionArea(bbox1, bbox2); 1730 | area = bboxArea(bbox1) + bboxArea(bbox2); 1731 | 1732 | // choose distribution with minimum overlap 1733 | if (overlap < minOverlap) { 1734 | minOverlap = overlap; 1735 | index = i; 1736 | 1737 | minArea = area < minArea ? area : minArea; 1738 | 1739 | } else if (overlap === minOverlap) { 1740 | // otherwise choose distribution with minimum area 1741 | if (area < minArea) { 1742 | minArea = area; 1743 | index = i; 1744 | } 1745 | } 1746 | } 1747 | 1748 | return index; 1749 | }, 1750 | 1751 | // sorts node children by the best axis for split 1752 | _chooseSplitAxis: function (node, m, M) { 1753 | 1754 | var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX, 1755 | compareMinY = node.leaf ? this.compareMinY : compareNodeMinY, 1756 | xMargin = this._allDistMargin(node, m, M, compareMinX), 1757 | yMargin = this._allDistMargin(node, m, M, compareMinY); 1758 | 1759 | // if total distributions margin value is minimal for x, sort by minX, 1760 | // otherwise it's already sorted by minY 1761 | if (xMargin < yMargin) node.children.sort(compareMinX); 1762 | }, 1763 | 1764 | // total margin of all possible split distributions where each node is at least m full 1765 | _allDistMargin: function (node, m, M, compare) { 1766 | 1767 | node.children.sort(compare); 1768 | 1769 | var toBBox = this.toBBox, 1770 | leftBBox = distBBox(node, 0, m, toBBox), 1771 | rightBBox = distBBox(node, M - m, M, toBBox), 1772 | margin = bboxMargin(leftBBox) + bboxMargin(rightBBox), 1773 | i, child; 1774 | 1775 | for (i = m; i < M - m; i++) { 1776 | child = node.children[i]; 1777 | extend(leftBBox, node.leaf ? toBBox(child) : child); 1778 | margin += bboxMargin(leftBBox); 1779 | } 1780 | 1781 | for (i = M - m - 1; i >= m; i--) { 1782 | child = node.children[i]; 1783 | extend(rightBBox, node.leaf ? toBBox(child) : child); 1784 | margin += bboxMargin(rightBBox); 1785 | } 1786 | 1787 | return margin; 1788 | }, 1789 | 1790 | _adjustParentBBoxes: function (bbox, path, level) { 1791 | // adjust bboxes along the given tree path 1792 | for (var i = level; i >= 0; i--) { 1793 | extend(path[i], bbox); 1794 | } 1795 | }, 1796 | 1797 | _condense: function (path) { 1798 | // go through the path, removing empty nodes and updating bboxes 1799 | for (var i = path.length - 1, siblings; i >= 0; i--) { 1800 | if (path[i].children.length === 0) { 1801 | if (i > 0) { 1802 | siblings = path[i - 1].children; 1803 | siblings.splice(siblings.indexOf(path[i]), 1); 1804 | 1805 | } else this.clear(); 1806 | 1807 | } else calcBBox(path[i], this.toBBox); 1808 | } 1809 | }, 1810 | 1811 | _initFormat: function (format) { 1812 | // data format (minX, minY, maxX, maxY accessors) 1813 | 1814 | // uses eval-type function compilation instead of just accepting a toBBox function 1815 | // because the algorithms are very sensitive to sorting functions performance, 1816 | // so they should be dead simple and without inner calls 1817 | 1818 | var compareArr = ['return a', ' - b', ';']; 1819 | 1820 | this.compareMinX = new Function('a', 'b', compareArr.join(format[0])); 1821 | this.compareMinY = new Function('a', 'b', compareArr.join(format[1])); 1822 | 1823 | this.toBBox = new Function('a', 1824 | 'return {minX: a' + format[0] + 1825 | ', minY: a' + format[1] + 1826 | ', maxX: a' + format[2] + 1827 | ', maxY: a' + format[3] + '};'); 1828 | } 1829 | }; 1830 | 1831 | function findItem(item, items, equalsFn) { 1832 | if (!equalsFn) return items.indexOf(item); 1833 | 1834 | for (var i = 0; i < items.length; i++) { 1835 | if (equalsFn(item, items[i])) return i; 1836 | } 1837 | return -1; 1838 | } 1839 | 1840 | // calculate node's bbox from bboxes of its children 1841 | function calcBBox(node, toBBox) { 1842 | distBBox(node, 0, node.children.length, toBBox, node); 1843 | } 1844 | 1845 | // min bounding rectangle of node children from k to p-1 1846 | function distBBox(node, k, p, toBBox, destNode) { 1847 | if (!destNode) destNode = createNode(null); 1848 | destNode.minX = Infinity; 1849 | destNode.minY = Infinity; 1850 | destNode.maxX = -Infinity; 1851 | destNode.maxY = -Infinity; 1852 | 1853 | for (var i = k, child; i < p; i++) { 1854 | child = node.children[i]; 1855 | extend(destNode, node.leaf ? toBBox(child) : child); 1856 | } 1857 | 1858 | return destNode; 1859 | } 1860 | 1861 | function extend(a, b) { 1862 | a.minX = Math.min(a.minX, b.minX); 1863 | a.minY = Math.min(a.minY, b.minY); 1864 | a.maxX = Math.max(a.maxX, b.maxX); 1865 | a.maxY = Math.max(a.maxY, b.maxY); 1866 | return a; 1867 | } 1868 | 1869 | function compareNodeMinX(a, b) { return a.minX - b.minX; } 1870 | function compareNodeMinY(a, b) { return a.minY - b.minY; } 1871 | 1872 | function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); } 1873 | function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); } 1874 | 1875 | function enlargedArea(a, b) { 1876 | return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * 1877 | (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY)); 1878 | } 1879 | 1880 | function intersectionArea(a, b) { 1881 | var minX = Math.max(a.minX, b.minX), 1882 | minY = Math.max(a.minY, b.minY), 1883 | maxX = Math.min(a.maxX, b.maxX), 1884 | maxY = Math.min(a.maxY, b.maxY); 1885 | 1886 | return Math.max(0, maxX - minX) * 1887 | Math.max(0, maxY - minY); 1888 | } 1889 | 1890 | function contains(a, b) { 1891 | return a.minX <= b.minX && 1892 | a.minY <= b.minY && 1893 | b.maxX <= a.maxX && 1894 | b.maxY <= a.maxY; 1895 | } 1896 | 1897 | function intersects(a, b) { 1898 | return b.minX <= a.maxX && 1899 | b.minY <= a.maxY && 1900 | b.maxX >= a.minX && 1901 | b.maxY >= a.minY; 1902 | } 1903 | 1904 | function createNode(children) { 1905 | return { 1906 | children: children, 1907 | height: 1, 1908 | leaf: true, 1909 | minX: Infinity, 1910 | minY: Infinity, 1911 | maxX: -Infinity, 1912 | maxY: -Infinity 1913 | }; 1914 | } 1915 | 1916 | // sort an array so that items come in groups of n unsorted items, with groups sorted between each other; 1917 | // combines selection algorithm with binary divide & conquer approach 1918 | 1919 | function multiSelect(arr, left, right, n, compare) { 1920 | var stack = [left, right], 1921 | mid; 1922 | 1923 | while (stack.length) { 1924 | right = stack.pop(); 1925 | left = stack.pop(); 1926 | 1927 | if (right - left <= n) continue; 1928 | 1929 | mid = left + Math.ceil((right - left) / n / 2) * n; 1930 | quickselect(arr, mid, left, right, compare); 1931 | 1932 | stack.push(left, mid, mid, right); 1933 | } 1934 | } 1935 | 1936 | },{"quickselect":11}]},{},[2]); 1937 | -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kronick/HexgridHeatmap/da4ef7d1243ec1c56f566e39d473fb2d68098a1d/docs/.DS_Store -------------------------------------------------------------------------------- /docs/hexgrid-heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kronick/HexgridHeatmap/da4ef7d1243ec1c56f566e39d473fb2d68098a1d/docs/hexgrid-heatmap.png -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hexgrid Heatmap 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | window.HexgridHeatmap = require('./HexgridHeatmap'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexgrid-heatmap", 3 | "version": "0.1.2", 4 | "description": "A hexgrid-based heatmap layer for your Mapbox GL JS map", 5 | "main": "HexgridHeatmap.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "browserify index.js -o dist/HexgridHeatmap.js", 9 | "dev": "watchify index.js -o dist/HexgridHeatmap.js --verbose" 10 | }, 11 | "author": "Sam Kronick (@kronick)", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@turf/center": "^3.10.3", 15 | "@turf/destination": "^3.10.3", 16 | "@turf/distance": "^3.10.3", 17 | "@turf/hex-grid": "^3.10.3", 18 | "rbush": "^2.0.1" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/kronick/HexgridHeatmap.git" 23 | }, 24 | "author": "Sam Kronick (http://samkronick.com)", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/kronick/HexgridHeatmap/issues" 28 | }, 29 | "homepage": "https://github.com/kronick/HexgridHeatmap#readme" 30 | } 31 | --------------------------------------------------------------------------------