Published
Edited
Sep 11, 2021
2 forks
28 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
snyderP = 1.0 + params.altitude / earthRadius;
Insert cell
Insert cell
Insert cell
Insert cell
dY = params.altitude * Math.sin(params.tilt / degrees);
Insert cell
Insert cell
dZ = params.altitude * Math.cos(params.tilt / degrees);
Insert cell
Insert cell
visibleYextent = 2 * dZ * Math.tan(0.5 * params.fieldOfView / degrees)
Insert cell
scale = earthRadius * numPixelsY / visibleYextent;
Insert cell
Insert cell
yShift = dY * numPixelsY / visibleYextent;
Insert cell
projection = d3.geoSatellite()
.scale(scale)
.translate([width / 2, yShift + numPixelsY / 2])
.rotate([-params.longitude, -params.latitude, params.rotation])
.tilt(params.tilt)
.distance(snyderP)
.preclip(preclip)
.precision(0.1)
Insert cell
Insert cell
Insert cell
preclip = {
const tilt = params.tilt / degrees;
const alpha = Math.acos(snyderP * Math.cos(tilt) * 0.999);
const clipDistance = geoClipCircle(Math.acos(1 / snyderP) - 1e-6);
return alpha ? geoPipeline(
clipDistance,
geoRotatePhi(Math.PI + tilt),
geoClipCircle(Math.PI - alpha - 1e-4), // Extra safety factor needed for large tilt values
geoRotatePhi(-Math.PI - tilt)
) : clipDistance;
}
Insert cell
function geoPipeline(...transforms) { // Move to Appendix?
return sink => {
for (let i = transforms.length - 1; i >= 0; --i) {
sink = transforms[i](sink);
}
return sink;
};
}
Insert cell
geoClipCircle = d3.geoClipCircle;
Insert cell
function geoRotatePhi(deltaPhi) {
const cosDeltaPhi = Math.cos(deltaPhi);
const sinDeltaPhi = Math.sin(deltaPhi);
return sink => ({
point(lambda, phi) {
const cosPhi = Math.cos(phi);
const x = Math.cos(lambda) * cosPhi;
const y = Math.sin(lambda) * cosPhi;
const z = Math.sin(phi);
const k = z * cosDeltaPhi + x * sinDeltaPhi;
sink.point(Math.atan2(y, x * cosDeltaPhi - z * sinDeltaPhi), Math.asin(k));
},
lineStart() { sink.lineStart(); },
lineEnd() { sink.lineEnd(); },
polygonStart() { sink.polygonStart(); },
polygonEnd() { sink.polygonEnd(); },
sphere() { sink.sphere(); }
});
}
Insert cell
Insert cell
earthRadius = 6371; // Spherical approximation: average radius in km
Insert cell
numPixelsY = width * 0.6;
Insert cell
degrees = 180 / Math.PI;
Insert cell
grid = ({
major: d3.geoGraticule().step([15,15])(),
minor: d3.geoGraticule().step([5,5])(),
horizon: ({type: "Sphere"})
})
Insert cell
land = Generators.observe(notify => {
const mousedown = event => { event.target.form && notify(land110); };
const mouseup = event => { event.target.form && notify(land50); };
notify(land50);
window.addEventListener("mousedown", mousedown);
window.addEventListener("mouseup", mouseup);
return () => {
window.removeEventListener("mousedown", mousedown);
window.removeEventListener("mouseup", mouseup);
};
})
Insert cell
land50 = fetch("https://cdn.jsdelivr.net/npm/world-atlas@1/world/50m.json")
.then(response => response.json())
.then(world => topojson.feature(world, world.objects.land));
Insert cell
land110 = fetch("https://cdn.jsdelivr.net/npm/world-atlas@1/world/110m.json")
.then(response => response.json())
.then(world => topojson.feature(world, world.objects.land));
Insert cell
topojson = require("topojson-client@3")
Insert cell
d3 = require("d3-geo@1", "d3-geo-projection@2")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more