Skip to content

Instantly share code, notes, and snippets.

@jameslaneconkling
Created October 18, 2017 01:55
Show Gist options
  • Save jameslaneconkling/3a3c410645bfa9bcb579dc481a3c9c72 to your computer and use it in GitHub Desktop.
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
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