UNPKG

hexbin

Version:

Helper library to find hexagon grid in which particular point lies

260 lines (243 loc) 9.54 kB
var turf = require('@turf/helpers'); const md5 = require('md5'); const cosines = []; const sines = []; for (let i = 0; i < 6; i++) { const angle = 2 * Math.PI / 6 * i; cosines.push(Math.cos(angle)); sines.push(Math.sin(angle)); } const hexagonAngle = 0.523598776; //30 degrees in radians function getHexBin(feature, cellSize, isMeters){ var point = feature.geometry.coordinates; let degreesCellSize; if(isMeters){ degreesCellSize = (cellSize/1000)/(111.111 * Math.cos(point[1] * Math.PI / 180)); } else { degreesCellSize = cellSize; } finalHexRootPoint = getSelectedHexagon(point[1],point[0],degreesCellSize); data= hexagon(finalHexRootPoint,degreesCellSize,degreesCellSize,null,cosines,sines); return data; } //here x and y is inverse, x is latitude and y is longitude function getSelectedHexagon(x, y, degreesCellSize){ var xinverse, yinverse = false; if(x < 0){ xinverse = true; x = -x; } if(y < 0){ yinverse = true; y = -y; } var hexRootPoint = getMoldulusHexagon(x,y,degreesCellSize); if(xinverse){ hexRootPoint[1] = -hexRootPoint[1]; } if(yinverse){ hexRootPoint[0] = -hexRootPoint[0]; } return hexRootPoint; } //here x and y is inverse, x is latitude and y is longitude function getMoldulusHexagon(x, y, degreesCellSize) { //y = y - (degreesCellSize / 2); //decrease hlaf cellsize because our grid is not starting from 0,0 which is having half hexagon var c = Math.sin(hexagonAngle) * degreesCellSize; //height between side length and hex top point var gridHeight = degreesCellSize + c; var halfWidth = Math.cos(hexagonAngle) * degreesCellSize; var gridWidth = halfWidth * 2; // Find the row and column of the box that the point falls in. var row; var column; if (y < (degreesCellSize / 2)){ row = -1; if(x < halfWidth) { column = 0; } else { column = Math.ceil( (x - halfWidth) / gridWidth); } } else { y = y - (degreesCellSize / 2); row = Math.floor(y / gridHeight); var rowIsOdd = row % 2 == 1; // Is the row an odd number? if (rowIsOdd)// Yes: Offset x to match the indent of the row column = Math.floor((x - halfWidth) / gridWidth); else// No: Calculate normally column = Math.floor(x / gridWidth); // Work out the position of the point relative to the box it is in var relY = y - (row * gridHeight) //- (degreesCellSize / 2);//decrease half cellsize because our grid is not starting from 0,0 which is having half hexagon var relX; if (rowIsOdd) { relX = (x - (column * gridWidth)) - halfWidth; } else { relX = x - (column * gridWidth); } var m = c / halfWidth; if (relY < (-m * relX) + c) // LEFT edge { row--; if (!rowIsOdd && row > 0){ column--; } } else if (relY < (m * relX) - c) // RIGHT edge { row--; if (rowIsOdd || row < 0){ column++; } } } //console.log("hexagon row " + row + " , column " + column); var lat = (column * gridWidth + ((row % 2) * halfWidth)) + halfWidth; var lon = (row * (c + degreesCellSize)) + c + (degreesCellSize); return [round(lon,6),round(lat,6)]; } function round(value, decimals) { return Number(Math.round(value+'e'+decimals)+'e-'+decimals); } /** * Creates hexagon * * @private * @param {Array<number>} center of the hexagon * @param {number} rx half hexagon width * @param {number} ry half hexagon height * @param {Object} properties passed to each hexagon * @param {Array<number>} cosines precomputed * @param {Array<number>} sines precomputed * @returns {Feature<Polygon>} hexagon */ function hexagon(center, rx, ry, properties, cosines, sines) { const vertices = []; for (let i = 0; i < 6; i++) { const x = round(center[0] + rx * cosines[i],6); const y = round(center[1] + ry * sines[i],6); vertices.push([x, y]); } //first and last vertex must be the same vertices.push(vertices[0].slice()); var feature = turf.polygon([vertices], properties); feature.properties.centroid = center; return feature; } function calculateHexGrids(features, cellSize, isAddIds, groupByProperty, cellSizeLatitude, existingHexFeatures){ var gridMap=[]; if(existingHexFeatures && Array.isArray(existingHexFeatures)){ existingHexFeatures.forEach(function (hexFeature){ gridMap[hexFeature.id] = hexFeature; }); } let maxCount = 0; //let minCount = Number.MAX_SAFE_INTEGER; let groupPropertyCount = {}; const degreesCellSize = (cellSize/1000)/(111.111 * Math.cos(cellSizeLatitude * Math.PI / 180)); features.forEach(function (feature, i){ if (feature.geometry.type.toLowerCase() === 'point') { if(!(feature.properties != null && feature.properties['@ns:com:here:xyz'] != null && feature.properties['@ns:com:here:xyz'].tags != null && feature.properties['@ns:com:here:xyz'].tags.includes('centroid'))){ var x = getHexBin(feature, degreesCellSize, false); if (x) { var gridId = md5(JSON.stringify(x.geometry)); x.id = gridId; if (!x.properties) { x.properties = {}; x.properties['count'] = 0; } var outGrid = x; if (gridMap[gridId]) { outGrid = gridMap[gridId]; } else { if (isAddIds) { outGrid.properties.ids = new Array(); } gridMap[gridId] = outGrid; outGrid.properties.count = 0; } outGrid.properties.count = outGrid.properties.count + 1; if(outGrid.properties.count > maxCount){ maxCount = outGrid.properties.count; } /* if(outGrid.properties.count < minCount){ minCount = outGrid.properties.count; }*/ if (isAddIds) { outGrid.properties.ids.push(feature.id); } //GroupBy property logic //console.log(groupByProperty); if(groupByProperty){ let propertyValue = feature.properties[groupByProperty]; //console.log(propertyValue); if (groupPropertyCount[propertyValue] == null || groupPropertyCount[propertyValue].maxCount == null) { groupPropertyCount[propertyValue] = {}; groupPropertyCount[propertyValue].maxCount = 0; } if(outGrid.properties.subcount == null) { outGrid.properties.subcount = {}; } if(outGrid.properties.subcount[propertyValue] == null){ outGrid.properties.subcount[propertyValue] = {}; outGrid.properties.subcount[propertyValue].count = 0; } outGrid.properties.subcount[propertyValue].count++; if(outGrid.properties.subcount[propertyValue].count > groupPropertyCount[propertyValue].maxCount){ groupPropertyCount[propertyValue].maxCount = outGrid.properties.subcount[propertyValue].count; } } gridMap[gridId] = outGrid; } else { console.error("something went wrong and hexgrid is not available for feature - " + feature); throw new Error("something went wrong and hexgrid is not available for feature - " + feature); } } } }); var hexFeatures=new Array(); for(var k in gridMap){ var feature = gridMap[k]; //feature.properties.minCount = minCount; feature.properties.maxCount = maxCount; feature.properties.occupancy = feature.properties.count/maxCount; feature.properties.color = "hsla(" + (200 - Math.round(feature.properties.occupancy*100*2)) + ", 100%, 50%,0.51)"; hexFeatures.push(feature); if(groupByProperty){ for (const key of Object.keys(feature.properties.subcount)) { feature.properties.subcount[key].maxCount = groupPropertyCount[key].maxCount; feature.properties.subcount[key].occupancy = feature.properties.subcount[key].count/groupPropertyCount[key].maxCount; feature.properties.subcount[key].color = "hsla(" + (200 - Math.round(feature.properties.subcount[key].occupancy*100*2)) + ", 100%, 50%,0.51)"; //console.log(key, JSON.stringify(feature.properties.subcount[key])); } } } return hexFeatures; } /** //var point = [13.4015825,52.473507]; var point = [ //13.4015825,52.473507 //0.4015825,0.473507 //13.401877284049988, // 52.473625332625154 //13.401110172271729, // 52.47341620511857 //13.401729762554169, // 52.47346521946711 0.003519058227539062, 0.0005149841308648958 ]; let feature = {'geometry':{'coordinates':point,'type':'Point'},'properties':{},'type':'Feature'}; //console.log(feature); var result = getHexBin(feature,100); //console.log(JSON.stringify(result)); var features = []; features.push(feature); features.push(result); var featureCollection = {'type':'FeatureCollection','features':features}; console.log(JSON.stringify(featureCollection, null, 2)); */ module.exports.getHexBin = getHexBin; module.exports.calculateHexGrids = calculateHexGrids;