Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active January 18, 2023 06:35
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save mbostock/2b85250396c17a79155302f91ec21224 to your computer and use it in GitHub Desktop.
Save mbostock/2b85250396c17a79155302f91ec21224 to your computer and use it in GitHub Desktop.
GeoJSON in Three.js
license: gpl-3.0
height: 960
border: no
redirect: https://observablehq.com/@mbostock/geojson-in-three-js

This example demonstrates how to display a GeoJSON MultiLineString geometry object as a wireframe using Three.js. Each point in the GeoJSON is converted from spherical coordinates (longitude and latitude in degrees) to three-dimensional Cartesian coordinates as follows:

x = cos(φ)cos(λ)
y = cos(φ)sin(λ)
z = sin(φ)

GeoJSON is often represented in equirectangular coordinates, which are planar rather than spherical. A visual artifact of this is extra lines along the antimeridian. You can convert equirectangular coordinates to spherical coordinates using d3.geoStitch; the world-atlas TopoJSON used in this example is already stitched. See also antimeridian cutting.

<!DOCTYPE html>
<body>
<script src="https://unpkg.com/three@0.84"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://unpkg.com/d3-array@1"></script>
<script src="https://unpkg.com/d3-collection@1"></script>
<script src="https://unpkg.com/d3-dispatch@1"></script>
<script src="https://unpkg.com/d3-request@1"></script>
<script src="https://unpkg.com/d3-timer@1"></script>
<script>
var width = 960,
height = 960,
radius = 228,
mesh,
graticule,
scene = new THREE.Scene,
camera = new THREE.PerspectiveCamera(70, width / height, 1, 1000),
renderer = new THREE.WebGLRenderer({alpha: true});
camera.position.z = 400;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
d3.json("https://unpkg.com/world-atlas@1/world/50m.json", function(error, topology) {
if (error) throw error;
scene.add(graticule = wireframe(graticule10(), new THREE.LineBasicMaterial({color: 0xaaaaaa})));
scene.add(mesh = wireframe(topojson.mesh(topology, topology.objects.land), new THREE.LineBasicMaterial({color: 0xff0000})));
d3.timer(function(t) {
graticule.rotation.x = mesh.rotation.x = Math.sin(t / 11000) * Math.PI / 3 - Math.PI / 2;
graticule.rotation.z = mesh.rotation.z = t / 10000;
renderer.render(scene, camera);
});
});
// Converts a point [longitude, latitude] in degrees to a THREE.Vector3.
function vertex(point) {
var lambda = point[0] * Math.PI / 180,
phi = point[1] * Math.PI / 180,
cosPhi = Math.cos(phi);
return new THREE.Vector3(
radius * cosPhi * Math.cos(lambda),
radius * cosPhi * Math.sin(lambda),
radius * Math.sin(phi)
);
}
// Converts a GeoJSON MultiLineString in spherical coordinates to a THREE.LineSegments.
function wireframe(multilinestring, material) {
var geometry = new THREE.Geometry;
multilinestring.coordinates.forEach(function(line) {
d3.pairs(line.map(vertex), function(a, b) {
geometry.vertices.push(a, b);
});
});
return new THREE.LineSegments(geometry, material);
}
// See https://github.com/d3/d3-geo/issues/95
function graticule10() {
var epsilon = 1e-6,
x1 = 180, x0 = -x1, y1 = 80, y0 = -y1, dx = 10, dy = 10,
X1 = 180, X0 = -X1, Y1 = 90, Y0 = -Y1, DX = 90, DY = 360,
x = graticuleX(y0, y1, 2.5), y = graticuleY(x0, x1, 2.5),
X = graticuleX(Y0, Y1, 2.5), Y = graticuleY(X0, X1, 2.5);
function graticuleX(y0, y1, dy) {
var y = d3.range(y0, y1 - epsilon, dy).concat(y1);
return function(x) { return y.map(function(y) { return [x, y]; }); };
}
function graticuleY(x0, x1, dx) {
var x = d3.range(x0, x1 - epsilon, dx).concat(x1);
return function(y) { return x.map(function(x) { return [x, y]; }); };
}
return {
type: "MultiLineString",
coordinates: d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X)
.concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y))
.concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) { return Math.abs(x % DX) > epsilon; }).map(x))
.concat(d3.range(Math.ceil(y0 / dy) * dy, y1 + epsilon, dy).filter(function(y) { return Math.abs(y % DY) > epsilon; }).map(y))
};
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment