Skip to content Skip to sidebar Skip to footer

Polygon Triangulation For Globe

Is it real to fill all polygons? Codepen. As I get it ThreeGeoJSON can not fill polygons, outlines only. Also I've tried Earcut for triangulation. drawThreeGeo(data, radius, 'spher

Solution 1:

I suggest you to use better map: countries.geojson

The solution consists of following steps, for each shape:

  1. Put vertices inside of shape, so that when triangulated, it could bend around the globe,
  2. Run https://github.com/mapbox/delaunator to build triangulated mesh,
  3. Step 2 will create triangles outside the shape too, we need to remove them by looking into each triangle, and deciding if it belongs to shape or not,
  4. Bend the triangulated mesh with convertCoordinates

You can test my jsfiddle: http://jsfiddle.net/mmalex/pg5a4132/

Warning: it is quite slow because of high level of detail of input.

triangulate planet three.js with delaunay triangulation

triangulate planet three.js wireframe

hover triangulated shape with three.js

The complete solution:

/* Draw GeoJSON

Iterates through the latitude and longitude values, converts the values to XYZ coordinates, and draws the geoJSON geometries.

*/letTRIANGULATION_DENSITY = 5; // make it smaller for more dense meshfunctionverts2array(coords) {
    let flat = [];
    for (let k = 0; k < coords.length; k++) {
        flat.push(coords[k][0], coords[k][1]);
    }
    return flat;
}

functionarray2verts(arr) {
    let coords = [];
    for (let k = 0; k < arr.length; k += 2) {
        coords.push([arr[k], arr[k + 1]]);
    }
    return coords;
}

functionfindBBox(points) {
    let min = {
        x: 1e99,
        y: 1e99
    };
    let max = {
        x: -1e99,
        y: -1e99
    };
    for (var point_num = 0; point_num < points.length; point_num++) {
        if (points[point_num][0] < min.x) {
            min.x = points[point_num][0];
        }
        if (points[point_num][0] > max.x) {
            max.x = points[point_num][0];
        }
        if (points[point_num][1] < min.y) {
            min.y = points[point_num][1];
        }
        if (points[point_num][1] > max.y) {
            max.y = points[point_num][1];
        }
    }
    return {
        min: min,
        max: max
    };
}

functionisInside(point, vs) {
    // ray-casting algorithm based on// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.htmlvar x = point[0],
        y = point[1];

    var inside = false;
    for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
        var xi = vs[i][0],
            yi = vs[i][1];
        var xj = vs[j][0],
            yj = vs[j][1];

        var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        if (intersect) inside = !inside;
    }

    return inside;
}

functiongenInnerVerts(points) {
    let res = [];
    for (let k = 0; k < points.length; k++) {
        res.push(points[k]);
    }

    let bbox = findBBox(points);

    let step = TRIANGULATION_DENSITY;
    let k = 0;
    for (let x = bbox.min.x + step / 2; x < bbox.max.x; x += step) {
        for (let y = bbox.min.y + step / 2; y < bbox.max.y; y += step) {
            let newp = [x, y];
            if (isInside(newp, points)) {
                res.push(newp);
            }
            k++;
        }
    }

    return res;
}

functionremoveOuterTriangles(delaunator, points) {
    let newTriangles = [];
    for (let k = 0; k < delaunator.triangles.length; k += 3) {
        let t0 = delaunator.triangles[k];
        let t1 = delaunator.triangles[k + 1];
        let t2 = delaunator.triangles[k + 2];

        let x0 = delaunator.coords[2 * t0];
        let y0 = delaunator.coords[2 * t0 + 1];

        let x1 = delaunator.coords[2 * t1];
        let y1 = delaunator.coords[2 * t1 + 1];

        let x2 = delaunator.coords[2 * t2];
        let y2 = delaunator.coords[2 * t2 + 1];

        let midx = (x0 + x1 + x2) / 3;
        let midy = (y0 + y1 + y2) / 3;

        let midp = [midx, midy];

        if (isInside(midp, points)) {
            newTriangles.push(t0, t1, t2);
        }
    }
    delaunator.triangles = newTriangles;
}

var x_values = [];
var y_values = [];
var z_values = [];

var progressEl = $("#progress");
var clickableObjects = [];
var someColors = [0x909090, 0x808080, 0xa0a0a0, 0x929292, 0x858585, 0xa9a9a9];

functiondrawThreeGeo(json, radius, shape, options) {

    var json_geom = createGeometryArray(json);
    var convertCoordinates = getConversionFunctionName(shape);

    for (var geom_num = 0; geom_num < json_geom.length; geom_num++) {
        console.log("Processing " + geom_num + " of " + json_geom.length + " shapes");

        // if (geom_num !== 17) continue;// if (geom_num > 10) break;if (json_geom[geom_num].type == 'Point') {
            convertCoordinates(json_geom[geom_num].coordinates, radius);
            drawParticle(y_values[0], z_values[0], x_values[0], options);

        } elseif (json_geom[geom_num].type == 'MultiPoint') {
            for (let point_num = 0; point_num < json_geom[geom_num].coordinates.length; point_num++) {
                convertCoordinates(json_geom[geom_num].coordinates[point_num], radius);
                drawParticle(y_values[0], z_values[0], x_values[0], options);
            }

        } elseif (json_geom[geom_num].type == 'LineString') {

            for (let point_num = 0; point_num < json_geom[geom_num].coordinates.length; point_num++) {
                convertCoordinates(json_geom[geom_num].coordinates[point_num], radius);
            }
            drawLine(y_values, z_values, x_values, options);

        } elseif (json_geom[geom_num].type == 'Polygon') {
            let group = createGroup(geom_num);
            let randomColor = someColors[Math.floor(someColors.length * Math.random())];

            for (let segment_num = 0; segment_num < json_geom[geom_num].coordinates.length; segment_num++) {

                let coords = json_geom[geom_num].coordinates[segment_num];
                let refined = genInnerVerts(coords);
                let flat = verts2array(refined);
                let d = newDelaunator(flat);
                removeOuterTriangles(d, coords);

                let delaunayVerts = array2verts(d.coords);
                for (let point_num = 0; point_num < delaunayVerts.length; point_num++) {
                    // convertCoordinates(refined[point_num], radius);convertCoordinates(delaunayVerts[point_num], radius);
                }
                // drawLine(y_values, z_values, x_values, options);drawMesh(group, y_values, z_values, x_values, d.triangles, randomColor);
            }

        } elseif (json_geom[geom_num].type == 'MultiLineString') {
            for (let segment_num = 0; segment_num < json_geom[geom_num].coordinates.length; segment_num++) {
                let coords = json_geom[geom_num].coordinates[segment_num];
                for (let point_num = 0; point_num < coords.length; point_num++) {
                    convertCoordinates(json_geom[geom_num].coordinates[segment_num][point_num], radius);
                }
                drawLine(y_values, z_values, x_values);
            }

        } elseif (json_geom[geom_num].type == 'MultiPolygon') {
            let group = createGroup(geom_num);
            let randomColor = someColors[Math.floor(someColors.length * Math.random())];

            for (let polygon_num = 0; polygon_num < json_geom[geom_num].coordinates.length; polygon_num++) {
                for (let segment_num = 0; segment_num < json_geom[geom_num].coordinates[polygon_num].length; segment_num++) {

                    let coords = json_geom[geom_num].coordinates[polygon_num][segment_num];
                    let refined = genInnerVerts(coords);
                    let flat = verts2array(refined);
                    let d = newDelaunator(flat);
                    removeOuterTriangles(d, coords);

                    let delaunayVerts = array2verts(d.coords);
                    for (let point_num = 0; point_num < delaunayVerts.length; point_num++) {
                        // convertCoordinates(refined[point_num], radius);convertCoordinates(delaunayVerts[point_num], radius);
                    }
                    // drawLine(y_values, z_values, x_values, options);drawMesh(group, y_values, z_values, x_values, d.triangles, randomColor)
                }
            }
        } else {
            thrownewError('The geoJSON is not valid.');
        }

    }

    progressEl.text("Complete!");
}

functioncreateGeometryArray(json) {
    var geometry_array = [];

    if (json.type == 'Feature') {
        geometry_array.push(json.geometry);
    } elseif (json.type == 'FeatureCollection') {
        for (var feature_num = 0; feature_num < json.features.length; feature_num++) {
            geometry_array.push(json.features[feature_num].geometry);
        }
    } elseif (json.type == 'GeometryCollection') {
        for (var geom_num = 0; geom_num < json.geometries.length; geom_num++) {
            geometry_array.push(json.geometries[geom_num]);
        }
    } else {
        thrownewError('The geoJSON is not valid.');
    }
    //alert(geometry_array.length);return geometry_array;
}

functiongetConversionFunctionName(shape) {
    var conversionFunctionName;

    if (shape == 'sphere') {
        conversionFunctionName = convertToSphereCoords;
    } elseif (shape == 'plane') {
        conversionFunctionName = convertToPlaneCoords;
    } else {
        thrownewError('The shape that you specified is not valid.');
    }
    return conversionFunctionName;
}


functionconvertToSphereCoords(coordinates_array, sphere_radius) {
    var lon = coordinates_array[0];
    var lat = coordinates_array[1];

    x_values.push(Math.cos(lat * Math.PI / 180) * Math.cos(lon * Math.PI / 180) * sphere_radius);
    y_values.push(Math.cos(lat * Math.PI / 180) * Math.sin(lon * Math.PI / 180) * sphere_radius);
    z_values.push(Math.sin(lat * Math.PI / 180) * sphere_radius);
}

functionconvertToPlaneCoords(coordinates_array, radius) {
    var lon = coordinates_array[0];
    var lat = coordinates_array[1];
    var plane_offset = radius / 2;

    z_values.push((lat / 180) * radius);
    y_values.push((lon / 180) * radius);

}

functiondrawParticle(x, y, z, options) {
    var particle_geom = newTHREE.Geometry();
    particle_geom.vertices.push(newTHREE.Vector3(x, y, z));

    var particle_material = newTHREE.ParticleSystemMaterial(options);

    var particle = newTHREE.ParticleSystem(particle_geom, particle_material);
    scene.add(particle);

    clearArrays();
}

functiondrawLine(x_values, y_values, z_values, options) {
    var line_geom = newTHREE.Geometry();
    createVertexForEachPoint(line_geom, x_values, y_values, z_values);

    var line_material = newTHREE.LineBasicMaterial(options);
    var line = newTHREE.Line(line_geom, line_material);
    scene.add(line);

    clearArrays();
}

functioncreateGroup(idx) {
    var group = newTHREE.Group();
    group.userData.userText = "_" + idx;
    scene.add(group);
    return group;
}

functiondrawMesh(group, x_values, y_values, z_values, triangles, color) {
    var geometry = newTHREE.Geometry();

    for (let k = 0; k < x_values.length; k++) {
        geometry.vertices.push(
            newTHREE.Vector3(x_values[k], y_values[k], z_values[k])
        );
    }

    for (let k = 0; k < triangles.length; k += 3) {
        geometry.faces.push(newTHREE.Face3(triangles[k], triangles[k + 1], triangles[k + 2]));
    }

    geometry.computeVertexNormals()

    var mesh = newTHREE.Mesh(geometry, newTHREE.MeshLambertMaterial({
        side: THREE.DoubleSide,
        color: color,
        wireframe: true
    }));
    clickableObjects.push(mesh);
    group.add(mesh);

    clearArrays();
}

functioncreateVertexForEachPoint(object_geometry, values_axis1, values_axis2, values_axis3) {
    for (var i = 0; i < values_axis1.length; i++) {
        object_geometry.vertices.push(newTHREE.Vector3(values_axis1[i],
            values_axis2[i], values_axis3[i]));
    }
}

functionclearArrays() {
    x_values.length = 0;
    y_values.length = 0;
    z_values.length = 0;
}

var scene = newTHREE.Scene();
var raycaster = newTHREE.Raycaster();
var camera = newTHREE.PerspectiveCamera(32, window.innerWidth / window.innerHeight, 0.5, 1000);
var radius = 200;

camera.position.x = 140.7744005681177;
camera.position.y = 160.30950538100814;
camera.position.z = 131.8637122564268;

var renderer = newTHREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);

var light = newTHREE.HemisphereLight(0xffffbb, 0x080820, 1);
scene.add(light);

var light = newTHREE.AmbientLight(0x505050); // soft white light
scene.add(light);

var geometry = newTHREE.SphereGeometry(radius, 32, 32);

var material = newTHREE.MeshPhongMaterial({
    color: 0x1e90ff
});
var sphere = newTHREE.Mesh(geometry, material);
scene.add(sphere);
var test_json = $.getJSON("https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson", function(data) {
    drawThreeGeo(data, radius + 1, 'sphere', {
        color: 'yellow'
    })

});

var controls = newTHREE.TrackballControls(camera);
controls.rotateSpeed *= 0.5;
controls.zoomSpeed *= 0.5;
controls.panSpeed *= 0.5;
controls.minDistance = 10;
controls.maxDistance = 5000;

functionrender() {
    controls.update();
    requestAnimationFrame(render);
    renderer.setClearColor(0x1e90ff, 1);
    renderer.render(scene, camera);
}

render()

functionconvert_lat_lng(lat, lng, radius) {
    var phi = (90 - lat) * Math.PI / 180,
        theta = (180 - lng) * Math.PI / 180,
        position = newTHREE.Vector3();

    position.x = radius * Math.sin(phi) * Math.cos(theta);
    position.y = radius * Math.cos(phi);
    position.z = radius * Math.sin(phi) * Math.sin(theta);

    return position;
}

// this will be 2D coordinates of the current mouse position, [0,0] is middle of the screen.var mouse = newTHREE.Vector2();

var hoveredObj; // this objects is hovered at the moment// Following two functions will convert mouse coordinates// from screen to three.js system (where [0,0] is in the middle of the screen)functionupdateMouseCoords(event, coordsObj) {
    coordsObj.x = ((event.clientX - renderer.domElement.offsetLeft + 0.5) / window.innerWidth) * 2 - 1;
    coordsObj.y = -((event.clientY - renderer.domElement.offsetTop + 0.5) / window.innerHeight) * 2 + 1;
}

functiononMouseMove(event) {
    updateMouseCoords(event, mouse);

    latestMouseProjection = undefined;
    clickedObj = undefined;

    raycaster.setFromCamera(mouse, camera); {
        var intersects = raycaster.intersectObjects(clickableObjects);

        let setGroupColor = function(group, colorHex) {
            for (let i = 0; i < group.children.length; i++) {
                if (!group.children[i].userData.color) {
                    group.children[i].userData.color = hoveredObj.parent.children[i].material.color.clone();
                    group.children[i].material.color.set(colorHex);
                    group.children[i].material.needsUpdate = true;
                }
            }
        }

        let resetGroupColor = function(group) {
            // set all shapes of the group to initial colorfor (let i = 0; i < group.children.length; i++) {
                if (group.children[i].userData.color) {
                    group.children[i].material.color = group.children[i].userData.color;
                    delete group.children[i].userData.color;
                    group.children[i].material.needsUpdate = true;
                }
            }
        }

        if (intersects.length > 0) {
            latestMouseProjection = intersects[0].point;
            // reset colors for previously hovered groupif (hoveredObj) {
                resetGroupColor(hoveredObj.parent);
            }

            hoveredObj = intersects[0].object;
            if (!hoveredObj.parent) return;
            // set colors for hovered groupsetGroupColor(hoveredObj.parent, 0xff0000);
        } else {
            if (!hoveredObj || !hoveredObj.parent) return;

            // nothing is hovered => just reset colors on the last groupresetGroupColor(hoveredObj.parent);
            hoveredObj = undefined;
            console.log("<deselected>");
        }
    }
}

window.addEventListener('mousemove', onMouseMove, false);

Solution 2:

You'd need to split each country into a separate geometry, use a raycaster to find out which country the mouse is over, then change its material.color. You can see raycasting in action in this example with source code available on the bottom-right corner. The key lines in that example are:

function onDocumentMouseMove(event) {
    event.preventDefault();
    mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
    mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}

function render() {
    // find intersections
    raycaster.setFromCamera( mouse, camera );
    var intersects = raycaster.intersectObjects( scene.children );
    if ( intersects.length > 0 ) {
        if ( INTERSECTED != intersects[ 0 ].object ) {
            if ( INTERSECTED ) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
            INTERSECTED = intersects[ 0 ].object;
            INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
            INTERSECTED.material.emissive.setHex( 0xff0000 );
        }
    } else {
        if ( INTERSECTED ) INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
        INTERSECTED = null;
    }
    renderer.render( scene, camera );
}

Post a Comment for "Polygon Triangulation For Globe"