geojson3d
Version:
Renders GeoJsons using Three.js
395 lines (336 loc) • 11 kB
JavaScript
var THREE = require('three');
var d3 = require('d3');
var topojson = require('topojson');
var geo = require('./geo');
THREE.TrackballControls = require('three-trackballcontrols');
var earcut = require('earcut');
var defaultWidth = 640;
var defaultHeight = 480;
var materials = {
phong: function(color) {
return new THREE.MeshPhongMaterial(
{
color: color,
specular: 0x000000,
shininess: 60,
shading: THREE.SmoothShading,
transparent:true
}
)
},
meshLambert: function(color) {
return new THREE.MeshLambertMaterial({
color: color,
specular: 0x009900,
shininess: 30,
shading: THREE.SmoothShading,
transparent:true
});
},
meshWireFrame: function(color) {
return new THREE.MeshBasicMaterial({
color: color,
specular: 0x009900,
shininess: 30,
shading: THREE.SmoothShading,
wireframe:true,
transparent:true
});
},
meshBasic: function(color) {
return new THREE.MeshBasicMaterial({
color: color,
specular: 0x009900,
shininess: 30,
shading: THREE.SmoothShading,
transparent: true
});
}
};
var randomFunctions = {
color: function(d) {
return Math.random() * 16777216;
},
height: function(d) {
return Math.random() * 20;
}
};
var material = 'phong';
function onWindowResize(container, sceneObj) {
sceneObj.camera.aspect = container.clientWidth / container.clientHeight;
sceneObj.camera.updateProjectionMatrix();
sceneObj.renderer.setSize(container.clientWidth, container.clientHeight);
sceneObj.controls.handleResize();
sceneObj.renderer.render(sceneObj.scene, sceneObj.camera);
}
function animate(sceneObj) {
requestAnimationFrame(
() => animate(sceneObj));
sceneObj.controls.update();
}
function clearGroups(json, sceneObj) {
if (json) {
if (json.type === 'FeatureCollection') {
json.features.forEach(
function(feature) {
sceneObj.scene.remove(feature._group);
}
);
} else if (json.type === 'Topology') {
Object.keys(json.objects).forEach(
function(key) {
json.objects[key].geometries.forEach(
function(object) {
sceneObj.scene.remove(object._group);
}
);
}
);
}
}
sceneObj.renderer.render(
sceneObj.scene,
sceneObj.camera
);
}
/**
* Use triangulation from earcut. The default triangulation
* causes holes to appear in the drawn maps.
*/
THREE.ShapeUtils.triangulateShape = function ( contour, holes ) {
var i, il, dim = 2, array;
var holeIndices = [];
var points = [];
addPoints( contour );
for ( i = 0, il = holes.length; i < il; i ++ ) {
holeIndices.push( points.length / dim );
addPoints( holes[ i ] );
}
try {
array = earcut(points, holeIndices, dim);
} catch (err) {
console.warn(err)
}
var result = [];
for ( i = 0, il = array.length; i < il; i += 3 ) {
result.push(
array.slice(i, i + 3));
}
return result;
function addPoints( a ) {
var i, il = a.length;
for ( i = 0; i < il; i ++ ) {
points.push( a[ i ].x, a[ i ].y );
}
}
}
/**
* Add a shape to the scene.
* @param {*} group
* @param {*} shape
* @param {*} extrudeSettings
* @param {*} material
* @param {*} color
* @param {*} x
* @param {*} y
* @param {*} z
* @param {*} rx
* @param {*} ry
* @param {*} rz
* @param {*} s
*/
function addShape(group, shape, extrudeSettings, material, color, x, y, z, rx, ry, rz, s) {
try{
var geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
var mesh = new THREE.Mesh(geometry, materials[material](color));
// Add shadows
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.position.set(x, y, z);
mesh.rotation.set(rx, ry, rz);
mesh.scale.set(s, s, s);
group.add(mesh);
} catch (err) {
console.warn(err);
}
}
/**
* A feature is a unit of a FeatureCollection. Converts
* a feature to the appropriate three.js object and adds it to the scene.
* @param {*} sceneObj
* @param {*} feature
* @param {*} projection
* @param {*} functions
*/
function addFeature(sceneObj, feature, projection, functions=randomFunctions) {
var group = new THREE.Group();
sceneObj.scene.add(group);
var color;
var amount;
try {
color = functions.color(feature.properties);
} catch(err) {
console.log(err);
}
try {
amount = functions.height(feature.properties);
} catch(err) {
console.log(err);
}
var extrudeSettings = {
amount: amount,
bevelEnabled: false
};
var material = 'phong';
if (feature.geometry.type === 'Polygon') {
var shape = geo.createPolygonShape(feature.geometry.coordinates, projection);
addShape(group, shape, extrudeSettings, material, color, 0, 0, amount, Math.PI, 0, 0, 1);
} else if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates.forEach(function(polygon) {
var shape = geo.createPolygonShape(polygon, projection);
addShape(group, shape, extrudeSettings, material, color, 0, 0, amount, Math.PI, 0, 0, 1);
});
} else {
console.err('This tutorial only renders Polygons and MultiPolygons')
}
return group;
}
function draw(json_url, container, sceneObj, projectionStr, functions, preprojected=false) {
var width = container.clientWidth;
var height = container.clientHeight;
d3.json(json_url, function(data) {
clearGroups(data, sceneObj);
if (data.type === 'FeatureCollection') {
drawFeatureCollection(data, width, height, functions, sceneObj, projectionStr, preprojected);
} else if (data.type === 'Topology') {
drawTopology(data, width, height, functions, sceneObj, projectionStr, preprojected);
} else {
console.err('Only TopoJSON and GeoJSON FeatureCollections are supported');
}
sceneObj.renderer.render(sceneObj.scene, sceneObj.camera);
});
}
function drawFeatureCollection(data, width, height, functions, sceneObj, projectionStr, preprojected) {
var projection = null;
if (isFunction(projectionStr)) {
projection = projectionStr;
} else {
if (!preprojected) {
projection = geo.getProjection(data, width, height, projectionStr);
}
}
data.features.forEach(function(feature) {
var group = addFeature(sceneObj, feature, projection, functions);
feature._group = group;
});
}
function drawTopology(data, width, height, functions, sceneObj, projectionStr, preprojected) {
var geojson = topojson.feature(data, data.objects[Object.keys(data.objects)[0]]);
// if not preprojected, compute a projection
var projection = null;
if (isFunction(projectionStr)) {
projection = projectionStr;
} else {
if (!preprojected) {
projection = geo.getProjection(geojson, width, height, projectionStr);
}
}
Object.keys(data.objects).forEach(function(key) {
console.log(data.objects[key]);
data.objects[key].geometries.forEach(function(object) {
var feature = topojson.feature(data, object);
var group = addFeature(sceneObj, feature, projection, functions);
object._group = group;
});
});
}
var initScene = function (container, json_location, width=640, height=480) {
var camera, controls, scene, renderer;
var light, spotLight, ambientLight;
var cross;
container.style.width = String(width) + "px";
if (Number.isInteger(height)) {
container.style.height = String(height) + "px";
}
camera = new THREE.PerspectiveCamera( 70, container.clientWidth / container.clientHeight, 0.1, 10000);
camera.position.z = Math.min(container.clientWidth, container.clientHeight);
controls = new THREE.TrackballControls(camera, container);
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
controls.keys = [65, 83, 68];
// World
scene = new THREE.Scene();
// Lights
light = new THREE.DirectionalLight(0xffffff);
light.position.set(1, 1, 1);
scene.add(light);
spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(-1000, -1000, 1000);
spotLight.castShadow = true;
scene.add(spotLight);
ambientLight = new THREE.AmbientLight(0x333333);
scene.add(ambientLight);
// Renderer
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
// Shadows
renderer.shadowMapEnabled = true;
renderer.shadowMapSoft = true;
renderer.shadowCameraNear = 1;
renderer.shadowCameraFar = camera.far;
renderer.shadowCameraFov = 60;
renderer.shadowMapBias = 0.0025;
renderer.shadowMapDarkness = 0.5;
renderer.shadowMapWidth = 1024;
renderer.shadowMapHeight = 1024;
var sceneObj = {
camera: camera,
controls: controls,
scene: scene,
renderer: renderer,
light: light,
spotLight: spotLight,
ambientLight: ambientLight,
cross: cross
};
controls.addEventListener(
'change',
function () {
sceneObj.renderer.render(sceneObj.scene, sceneObj.camera);
}
);
window.addEventListener(
'resize',
function(ev) {
onWindowResize(container, sceneObj);
},
false
);
onWindowResize(container, sceneObj);
renderer.render(scene, camera);
return sceneObj
}
var isFunction = function(functionToCheck) {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}
var plot = function(container, json_location, width, height, projection=undefined, functions=randomFunctions, preprojected=false) {
var sceneObj = initScene(
container,
json_location,
width,
height
);
draw(json_location, container, sceneObj, projection, functions, preprojected);
animate(sceneObj);
}
exports.plot = plot;
exports.projections = geo.projections;
exports.randomFunctions = randomFunctions;