Created
October 18, 2017 01:55
-
-
Save jameslaneconkling/3a3c410645bfa9bcb579dc481a3c9c72 to your computer and use it in GitHub Desktop.
generate and project a myriahedral grid onto the globe, in the style of Buckminster Fuller's famous Dymaxion map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { | |
Readable | |
} = require('stream'); | |
const { | |
readFileSync | |
} = require('fs'); | |
const degrees2Radians = degrees => degrees * (Math.PI / 180); | |
const radians2Degrees = radians => radians * (180 / Math.PI); | |
const bearing = ([lon1, lat1], [lon2, lat2]) => { | |
const a = Math.sin(lon2 - lon1) * | |
Math.cos(lat2); | |
const b = Math.cos(lat1) * | |
Math.sin(lat2) - | |
Math.sin(lat1) * | |
Math.cos(lat2) * | |
Math.cos(lon2 - lon1); | |
return Math.atan2(a, b); | |
}; | |
const midDistance = ([lon1, lat1], [lon2, lat2]) => { | |
var dLat = lat2 - lat1; | |
var dLon = lon2 - lon1; | |
var a = Math.pow(Math.sin(dLat / 2), 2) + | |
Math.pow(Math.sin(dLon / 2), 2) * | |
Math.cos(lat1) * | |
Math.cos(lat2); | |
return Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | |
}; | |
/** | |
* Calculate midpoint between to geographic coordinates using the Haversine formula | |
* http://www.movable-type.co.uk/scripts/latlong.html | |
* | |
* @param {[number, number]} degreesPoint1 first point, as [degreesX, degreesY] (aka [degreesLng, degreesLat]) | |
* @param {[number, number]} degreesPoint2 second point, as [degreesX, degreesY] (aka [degreesLng, degreesLat]) | |
*/ | |
const midpoint = (degreesPoint1, degreesPoint2) => { | |
const radianPoint1 = degreesPoint1.map(degrees2Radians); | |
const radianPont2 = degreesPoint2.map(degrees2Radians); | |
var dist = midDistance(radianPoint1, radianPont2); | |
var heading = bearing(radianPoint1, radianPont2); | |
var lat2 = Math.asin( | |
Math.sin(radianPoint1[1]) * | |
Math.cos(dist) + | |
Math.cos(radianPoint1[1]) * | |
Math.sin(dist) * | |
Math.cos(heading) | |
); | |
var lon2 = radianPoint1[0] + Math.atan2( | |
Math.sin(heading) * | |
Math.sin(dist) * | |
Math.cos(radianPoint1[1]), | |
Math.cos(dist) - | |
Math.sin(radianPoint1[1]) * | |
Math.sin(lat2) | |
); | |
return [lon2, lat2].map(radians2Degrees); | |
}; | |
/** | |
* subdivide triangle into four triangles | |
* | |
* a a | |
* / \ / \ | |
* / \ ====> ab --- ac | |
* / \ / \ / \ | |
* b --------- c b --- bc --- c | |
* | |
*/ | |
const subdivideTriangle = ({ properties: { id }, geometry: { coordinates: [[a, b, c]] } }) => { | |
const ab = midpoint(a, b); | |
const bc = midpoint(b, c); | |
const ac = midpoint(a, c); | |
return [ | |
{ id, coordinates: [[a, ab, ac, a]] }, | |
{ id, coordinates: [[ab, b, bc, ab]] }, | |
{ id, coordinates: [[bc, ac, ab, bc]] }, | |
{ id, coordinates: [[ac, bc, c, ac]] } | |
] | |
.map(({ id, coordinates }, idx) => ({ | |
type: 'Feature', | |
properties: { id: `${id}.${idx + 1}` }, | |
geometry: { type: 'Polygon', coordinates } | |
})); | |
}; | |
// NOTE - the below closure shenanigans ensures the stringified features array does not have | |
// a trailing comma necessary so the output passes a json linter | |
const stringifyFeatures = (() => { | |
let first = true; | |
return features => features | |
.reduce((acc, feature) => { | |
if (first) { | |
first = false; | |
return `${acc}${JSON.stringify(feature)}`; | |
} | |
return `${acc},${JSON.stringify(feature)}`; | |
}, ''); | |
})(); | |
const createMyriahedronGenerator = function* createMyriahedronGenerator(triangles, depth) { | |
if (depth <= 1) { | |
yield triangles; | |
return; | |
} | |
for (let i = 0; i < triangles.length; i++) { | |
yield* createMyriahedronGenerator(subdivideTriangle(triangles[i]), depth - 1); | |
} | |
}; | |
/** | |
* Take an input icosahedron geoJSON and subdivide into a myriahedron of specified depth | |
* | |
* @param {Object} icosahedron input icosahedron geoJSON | |
* @param {number} depth specified depth | |
*/ | |
const generateMyriahedron = module.exports = (icosahedron, depth) => { | |
const readStream = Readable(); | |
readStream.push('{ "type": "FeatureCollection", "features": ['); | |
const myriahedronGenerator = createMyriahedronGenerator(icosahedron.features, depth); | |
readStream._read = () => { | |
const { value: features, done } = myriahedronGenerator.next(); | |
if (done) { | |
readStream.push('\n]}'); | |
readStream.push(null); | |
return; | |
} | |
readStream.push(stringifyFeatures(features)); | |
}; | |
return readStream; | |
}; | |
/** | |
* CLI bindings | |
*/ | |
const depth = parseInt(process.argv[2], 10); | |
if (isNaN(depth) || depth < 1) { | |
console.error('second depth argument must be an integer >= 1'); | |
process.exit(1); | |
} | |
let icosahedron; | |
try { | |
icosahedron = JSON.parse(readFileSync(process.argv[3])); | |
} catch (e) { | |
console.error('error reading or parsing file'); | |
console.error(e); | |
process.exit(1); | |
} | |
generateMyriahedron(icosahedron, depth).pipe(process.stdout); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment