One, foreword
In the mapbox style standard, the height attributes of spatial elements are only reflected in the fill-extrusion layer. If you want to add an elevation attribute to the scene, you cannot do it with annotations, dots, lines, or surfaces. Unless we customize layers to manipulate WebGL. I have also practiced in the wind and weather visualization practice (Zhihu, Excavation gold). It is undoubtedly a very painful thing to write coloring code and operate video memory by myself. We use three.js to extend the capabilities of MapBoxGL. Threebox is a great plugin to help us join mapBoxgl and three.js to easily add spatial objects to a scene. Threebox has provided multiple cases, and I wrote two cases during my test learning practice and encountered some problems, to summarize. Corrections are welcome.
- Draw a space with a guide hole
- Achieve an OD fly line effect
Draw a space body with island holes
Instead of using three.js to create the Shape object, use ExtrudeGeometry to pull the object up to its height. Create a geometry object by creating vertices, triangles.
If I use Threejs in MapBoxGL to create ExtrudeGeometry, specify how the pull height will be achieved. I’m just going to depth, how much is that?
1, three. Js cuboid
It is easier to draw a cuboid directly in Threejs, defining six vertices of a cuboid and twelve triangles.
let cubeGeometry = new THREE.Geometry();
// Create the vertices of the cube
let vertices = [
new THREE.Vector3(10.10.10), //v0
new THREE.Vector3(-10.10.10), //v1
new THREE.Vector3(-10, -10.10), //v2
new THREE.Vector3(10, -10.10), //v3
new THREE.Vector3(10, -10, -10), //v4
new THREE.Vector3(10.10, -10), //v5
new THREE.Vector3(-10.10, -10), //v6
new THREE.Vector3(-10, -10, -10) //v7
];
cubeGeometry.vertices = vertices;
// Create a cube face
let faces = [
new THREE.Face3(0.1.2),
new THREE.Face3(0.2.3),
new THREE.Face3(0.3.4),
new THREE.Face3(0.4.5),
new THREE.Face3(1.6.7),
new THREE.Face3(1.7.2),
new THREE.Face3(6.5.4),
new THREE.Face3(6.4.7),
new THREE.Face3(5.6.1),
new THREE.Face3(5.1.0),
new THREE.Face3(3.2.7),
new THREE.Face3(3.7.4)]; cubeGeometry.faces = faces;// Generate a normal vector
cubeGeometry.computeFaceNormals();
let cubeMaterial = new THREE.MeshLambertMaterial({ color: 0x00ffff });
cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);
Copy the code
2. Threebox
Draw a cuboid using ThreeBox and MapBoxGL. Specify the unit of side length: meters. Put the cuboid at 50 meters of latitude and longitude (120.6827139, 31.2970519).
map.addLayer({ id: 'custom_layer', type: 'custom', onAdd: function (map, mbxContext) { tb = new Threebox( map, mbxContext, { defaultLights: true } ); let geometry = new THREE.BoxGeometry(20, 20, 20); let redMaterial = new THREE.MeshPhongMaterial({ color: 0x009900, // side: THREE.DoubleSide }); let cube = new THREE.Mesh(geometry, redMaterial); Cube = tb.Object3D({obj: cube, units: 'meters',}).setcoords ([120.6827139, 31.2970519, 50]) // add(cube)}, render: function (gl, matrix) { tb.update(); }});Copy the code
3. Space body with island hole
To draw a polygon with a guide hole in MapBoxGL using ThreeBox, the polygon needs to be triangulated first. Just like drawing a cuboid, we need to specify the cuboid vertices and triangles, that is, which triangles are required to form a cube. This is a slightly more complicated algorithm. Cdt2d is a Delaunay triangulation library written in JavaScript. It can help us to triangulate polygons. And notice here, it’s a library of two dimensional triangulation algorithms.
1) Delaunay triangulation
let cdt2d = require('cdt2d')
// Specify the vertex coordinates
let points = [
[-2, -2], [...2.2],
[ 2.2],
[ 2, -2],
[ 1.0],
[ 0.1], [...1.0],
[ 0, -1]]// Specify edges, note vertex order, each face is a closed string of points
// The first four sides form a face -- the outer circle
// The last four edges form an inner circle
let edges = [
//Outer loop
[0.1].// The edge of the first vertex and the second vertex
[1.2],
[2.3],
[3.0].//Inner loop
[4.5],
[5.6],
[6.7],
[7.4]]// Outputs the triangulation results
[[0.3.7], [0.6.1], [0.7.6], [1.5.2], [1.6.5], [2.4.3], [2.5.4], [3.4.7]]
console.log(cdt2d(points, edges, {exterior: false}))
Copy the code
2) Draw polygons with island holes
The results and code are posted directly below. It may be easier to understand the code directly. Our goal is to build vertices and edges that we triangulate. Then, by using Delaunay triangulation algorithm, the triangles are obtained. Finally, create a three object from the vertices and facets results and add it to the scene. Building the edges for triangulation is a bit convoluted and easy to make mistakes, see the triangulation example above if you don’t understand.
The coordinates drawn in ThreeBox need to be converted to Web projection coordinates. The normalizeVertices function does two things. First, it creates an outsource ellipsoid that surrounds all vertices and calculates the center coordinates of the outsource ellipsoid. Then, subtract the center of the outer sphere from the original vertex coordinates (vector subtraction) to get the coordinates of the original vertex relative to the center of the outer sphere. The idea is that by setting the mesh’s center coordinate to be the center of the sphere, we can place our graph in a specific position, which is how ThreeBox draws objects on the map.
normalizeVertices(vertices) {
var geometry = new THREE.Geometry();
for (v3 of vertices) {
geometry.vertices.push(v3)
}
geometry.computeBoundingSphere();
var center = geometry.boundingSphere.center;
var radius = geometry.boundingSphere.radius;
var scaled = vertices.map(function(v3){
var normalized = v3.sub(center);
return normalized;
});
return {vertices: scaled, position: center}
},
Copy the code
map.on('style.load'.function () {
// Standard GeoJSON data
jQuery.get('.. /.. /data/dd2.json').then(data= > {
map.addLayer({
id: 'custom_layer'.type: 'custom'.onAdd: function (map, mbxContext) {
tb = new Threebox(
map,
mbxContext,
{ defaultLights: true}); data.features.forEach(feature= > {
let geoCor = feature.geometry.coordinates;
// Store vertices [lang,lat,0]
let vertexs = [];
// Store the vertex vector three.vector3 (x, y, z);
const vertexsVectors = [];
// Store graphic edges for triangulation
const edges = [];
// Store graph vertices for triangulation
const points = [];
// The vertices of a polygon with a guide hole are incremented, regardless of how many polygonal rows it has
let tindex = 0;
// Construct vertices and edges for triangulation, view the rules for the above triangulation, vertices and edges
geoCor.forEach((coors, i) = > {
let firstCoordIndex = tindex;
let max = coors.length - 1;
coors.forEach((coor, i) = > {
if (i == max) return;
vertexs.push([...coor, 0]);
if (i < max - 1) {
edges.push([tindex, (tindex + 1)]);
} else {
edges.push([tindex, firstCoordIndex]);
}
tindex++;
});
})
// Convert vertices latitude and longitude coordinates to Web Mercator projection coordinates
const worldCoors = tb.utils.lnglatsToWorld(vertexs);
// Normalize the world coordinates
const normalized = tb.utils.normalizeVertices(worldCoors);
const { vertices, position } = normalized;
for (let i = 0; i < vertices.length; i++) {
const { x, y, z } = vertices[i];
vertexsVectors.push(new THREE.Vector3(x, y, z));
points.push([x, y, z])
}
// Triangulate
let triangles = cdt2d(points, edges, { exterior: false });
let faces = [];
// Construct the triangle
triangles.forEach(item= > {
faces.push(newTHREE.Face3(... item)) })// Create a collection object with a guide hole and assign vertices and triangles
const geom = new THREE.Geometry();
geom.vertices = vertexsVectors;
geom.faces = faces;
// Compute the normal vector
geom.computeFaceNormals();
const mesh = new THREE.Mesh(
geom,
new THREE.MeshLambertMaterial({
color: 0x00ffff.// wireframe:true,})); mesh.castShadow =true;
// Sets the position of the object in the mapmesh.position.copy(position); tb.add(mesh); })},render: function (gl, matrix) { tb.update(); }}); })});Copy the code
3) Draw a multilateral body with island holes
The rendering body is a little bit more troublesome, mainly in getting all the triangles, if specified to pull up to 5 meters height. Same idea, the base is built, the vertices are not actually built, add the index of all the vertices of the triangle to the number of vertices on each face, and then build the side, which is not discussed here. Post my bad code.
data.features.forEach(feature= > {
let base = 0
let CG = 5;
let geoCor = feature.geometry.coordinates;
let arras = [];
let arrasbase = [];
let arrasTop = [];
const resVertices = [];
const edges = [];
const points = [];
let tindex = 0;
geoCor.forEach((coors, i) = > {
let firstCoordIndex = tindex;
let max = coors.length - 1;
coors.forEach((coor, i) = > {
if (i == max) return;
arrasbase.push([...coor, base]);
arrasTop.push([...coor, base + CG]);
if (i < max - 1) {
edges.push([tindex, (tindex + 1)]);
} else {
edges.push([tindex, firstCoordIndex]);
}
tindex++;
});
})
arras = [...arrasbase, ...arrasTop];
const worldCoors = tb.utils.lnglatsToWorld(arras);
const normalized = tb.utils.normalizeVertices(worldCoors);
const { vertices, position } = normalized;
for (let i = 0; i < vertices.length; i++) {
const { x, y, z } = vertices[i];
resVertices.push(new THREE.Vector3(x, y, z));
points.push([x, y, z])
}
let topEdges = [];
for (let item of edges) {
topEdges.push([item[0] + edges.length, item[1] + edges.length])
}
let resEdges = [...edges, ...topEdges]
points.length = points.length / 2;
let triangles = cdt2d(points, edges, { exterior: false });
let faces = [];
triangles.forEach(item= > {
faces.push(newTHREE.Face3(... item)) }) triangles.forEach(item= > {
faces.push(new THREE.Face3(item[0] + edges.length, item[1] + edges.length, item[2] + edges.length))
})
for (let i = 0; i < edges.length; i++) {
let [a, b] = edges[i];
faces.push(new THREE.Face3(a, b, a + edges.length))
faces.push(new THREE.Face3(a + edges.length, b, b + edges.length))
}
const geom = new THREE.Geometry();
geom.vertices = resVertices;
geom.faces = faces;
geom.computeFaceNormals();
const mesh = new THREE.Mesh(
geom,
new THREE.MeshLambertMaterial({
color: 0x00ffff.wireframe: true,})); mesh.castShadow =true;
mesh.position.copy(position);
tb.add(mesh);
})
Copy the code
4) Draw building standard layer data
Use the code above, modify it a little bit, change the data set, use the building standard layer data, and see what it looks like.
Using three.js new three. EdgesGeometry(geom) to add border lines to mesh, it becomes transparent. How to make him opaque. If it’s opaque and you can’t see the edges behind you, it won’t have such a messy effect.
3. OD flying line
Using the BUS passenger on and off OD data as an example, write a simple OD flying line effect. Tween.js is used in the animation. The function is relatively simple, directly look at the code, but also very easy to understand. The principle is to create an arc with three points, specify the number of interpolation points, use tween.js to specify the animation duration, and change the number of vertices in the arc. That’s it.
ID of the station where passengers get on the bus Longitude of the station Dimension ID of the station where passengers get off the bus Longitude of the station Dimension Number of people who get off the bus 002113 AD - FFF - d9b5 cb67-2, 31.31003, 120.7365, 013563 fc - a796 - ac43 - e4a2, 31.32696, 120.58606, 3Copy the code
mapboxmap.on('style.load'.function () {
jQuery.get('./data/od.data').then(data= > {
mapboxmap.addLayer({
id: 'custom_layer'.type: 'custom'.onAdd: function (map, mbxContext) {
this.map = map;
tb = new Threebox(
map,
mbxContext,
{ defaultLights: true});let lineGroup = draw(tb, data);
tb.add(lineGroup);
},
render: function (gl, matrix) {
if (this.map)
this.map.triggerRepaint(); tb.update(); TWEEN.update(); }}); })});function draw(tb, dataTxt, stationId = "4722ab93-c31f-457f-880a-47c5abfe5ae6") {
// let levelH = 10; // Level baseline height
let curveH = 10;
let lineGroup = new THREE.Group();
lineGroup.name = 'lineGroup';
dataTxt.split('\n').map(function (s, i) {
let splitArray = s.split(', ');
if (splitArray[0] !== stationId) {
return;
}
let ll_o = [parseFloat(splitArray[1]), parseFloat(splitArray[2])].reverse();
let xy_o = tb.utils.lnglatsToWorld([[...ll_o, 0]]);
let ll_d = [parseFloat(splitArray[4]), parseFloat(splitArray[5])].reverse();;
let xy_d = tb.utils.lnglatsToWorld([[...ll_d, 0]])
let count = parseFloat(splitArray[6]);
let color;
let opacity;
if (count > 0 && count <= 10) {
color = 60;
opacity = 0.3;
} else if (count > 10 && count <= 30) {
color = 50;
opacity = 0.6;
} else if (count > 30 && count <= 50) {
color = 40;
opacity = 0.8;
} else if (count > 50 && count <= 100) {
color = 30;
opacity = 0.8;
} else if (count > 100 && count <= 150) {
color = 20;
opacity = 0.8;
} else if (count > 150) {
color = 10;
opacity = 0.8;
}
let curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(xy_o[0].x, xy_o[0].y, 0),
new THREE.Vector3((xy_o[0].x + xy_d[0].x) / 2, (xy_o[0].y + xy_d[0].y) / 2, curveH),
new THREE.Vector3(xy_d[0].x, xy_d[0].y, 0)]);let geometry = new THREE.Geometry();
let curveModelData = curve.getPoints(50);
geometry.vertices = curveModelData //.slice(0, 1);
let material = new THREE.LineBasicMaterial({
color: new THREE.Color("hsl(" + color + ", 100%, 50%)"),
opacity: opacity,
transparent: true.linewidth: 3.blending: THREE.AdditiveBlending
});
let curveObject = new THREE.Line(geometry, material);
curveObject.geometry.verticesNeedUpdate = true;
let meshUserData = new Object(a); meshUserData.curveModelData = curveModelData; curveObject.userData = meshUserData; lineGroup.add(curveObject); tween =new TWEEN.Tween({ endPointIndex: 1 })
.to({ endPointIndex: 50 }, 3000)
.onUpdate(function (iii) {
let endPointIndex = Math.ceil(iii.endPointIndex);
let curvePartialData = new THREE.CatmullRomCurve3(curveModelData.slice(0, endPointIndex));
curveObject.geometry.vertices = curvePartialData.getPoints(50);
curveObject.geometry.verticesNeedUpdate = true;
})
.repeat(Infinity)
tween.start();
});
return lineGroup;
}
Copy the code
Four,
Use ThreeBox in MapBoxGL to implement custom layers that are not easy to implement in native MapBoxGL, or avoid directly manipulating WebGL in custom layers. Practice two simple cases, summary review, mistakes are unavoidable, welcome to exchange learning. If the future is combined with real business scenarios, we will share the results and communicate and learn together. Thank you.