diff --git a/src/Converter/Feature2Mesh.js b/src/Converter/Feature2Mesh.js index 263ee2f99c..aa0236c35b 100644 --- a/src/Converter/Feature2Mesh.js +++ b/src/Converter/Feature2Mesh.js @@ -7,6 +7,11 @@ const _color = new THREE.Color(); const maxValueUint8 = Math.pow(2, 8) - 1; const maxValueUint16 = Math.pow(2, 16) - 1; const maxValueUint32 = Math.pow(2, 32) - 1; +const quaternion = new THREE.Quaternion(); +const normal = new THREE.Vector3(0, 0, 0); +const v0 = new THREE.Vector3(0, 0, 0); +const v1 = new THREE.Vector3(0, 0, 0); +const vZ = new THREE.Vector3(0, 0, 1); function toColor(color) { if (color) { @@ -274,8 +279,33 @@ function featureToPolygon(feature, options) { const geomVertices = vertices.slice(start * 3, end * 3); const holesOffsets = geometry.indices.map(i => i.offset - start).slice(1); - const triangles = Earcut(geomVertices, holesOffsets, 3); + const reprojection = options.withoutPlanReprojection ? !options.withoutPlanReprojection : true; + if (reprojection) { + // transform all vertices to XY plan + // calculate average plane from points + // http://www.les-mathematiques.net/phorum/read.php?13,728203,728209#msg-728209 + const size = count * 3; + for (let i = 3; i < size; i += 3) { + v0.fromArray(geomVertices, i - 3); + v1.fromArray(geomVertices, i); + normal.x += (v0.y - v1.y) * (v0.z + v1.z); + normal.y += (v0.z - v1.z) * (v0.x + v1.x); + normal.z += (v0.x - v1.x) * (v0.y + v1.y); + } + + // calculate normal plane + normal.normalize(); + // calculate quaternion to transform the normal plane to z axis + quaternion.setFromUnitVectors(normal, vZ); + + // apply transformation to vertices + for (let i = 0; i < size; i += 3) { + v1.fromArray(geomVertices, i).applyQuaternion(quaternion); + v1.toArray(geomVertices, i); + } + } + const triangles = Earcut(geomVertices, holesOffsets, 3); const startIndice = indices.length; indices.length += triangles.length; @@ -442,6 +472,7 @@ export default { * a THREE.Group. * * @param {Object} options - options controlling the conversion + * @param {Boolean} [options.withoutPlanReprojection] - to avoid useless plan reprojection for non-vertical meshes * @param {function} [options.batchId] - optional function to create batchId attribute. It is passed the feature property and the feature index. As the batchId is using an unsigned int structure on 32 bits, the batchId could be between 0 and 4,294,967,295. * @return {function} * @example Example usage of batchId with featureId. diff --git a/src/Parser/GeoJsonParser.js b/src/Parser/GeoJsonParser.js index 674e52f77d..ec1c798e09 100644 --- a/src/Parser/GeoJsonParser.js +++ b/src/Parser/GeoJsonParser.js @@ -67,7 +67,15 @@ const toFeature = { // Then read contour and holes for (let i = 0; i < coordsIn.length; i++) { - this.populateGeometry(crsIn, coordsIn[i], geometry, feature); + // GeoJson standard: The first and last positions are equivalent, + // and they MUST contain identical values; their representation SHOULD also be identical. + const ring = coordsIn[i]; + if (ring.length > 1) { + if (ring[0].every((v, i) => v == ring[ring.length - 1][i])) { + ring.pop(); + } + } + this.populateGeometry(crsIn, ring, geometry, feature); } feature.updateExtent(geometry); }, diff --git a/test/data/geojson/holes.geojson.json b/test/data/geojson/holes.geojson.json index 71425cbc2f..cdff084a1e 100644 --- a/test/data/geojson/holes.geojson.json +++ b/test/data/geojson/holes.geojson.json @@ -12,19 +12,25 @@ 0 ], [ - 0, - 1 + 2, + 0 ], [ 2, 1 ], [ - 2, + 0, + 1 + ], + [ + 0, 0 ] ] ] + }, + "properties" : { } }, { @@ -38,19 +44,25 @@ 0.3 ], [ - 0.2, - 0.7 + 1.4, + 0.3 ], [ 1.4, 0.7 ], [ - 1.4, + 0.2, + 0.7 + ], + [ + 0.2, 0.3 ] ] ] + }, + "properties" : { } }, { @@ -64,15 +76,19 @@ 0 ], [ - 0, - 1 + 2, + 0 ], [ 2, 1 ], [ - 2, + 0, + 1 + ], + [ + 0, 0 ] ], @@ -92,9 +108,15 @@ [ 1.4, 0.3 + ], + [ + 0.2, + 0.3 ] ] ] + }, + "properties" : { } } ] diff --git a/test/data/geojson/simple.geojson.json b/test/data/geojson/simple.geojson.json index 004f327bde..52bb1397be 100644 --- a/test/data/geojson/simple.geojson.json +++ b/test/data/geojson/simple.geojson.json @@ -42,16 +42,16 @@ 43.715534726205114 ], [ - 0.9067382756620646, - 43.72744458647463 + 0.36291503347456455, + 43.15710884095329 ], [ 1.1044921819120646, 43.37311218382002 ], [ - 0.36291503347456455, - 43.15710884095329 + 0.9067382756620646, + 43.72744458647463 ], [ 0.30798339284956455, diff --git a/test/data/geojson/vertical.geojson.json b/test/data/geojson/vertical.geojson.json new file mode 100644 index 0000000000..f278cd79db --- /dev/null +++ b/test/data/geojson/vertical.geojson.json @@ -0,0 +1,42 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 0, + 0, + 0 + ], + [ + 0, + 0, + 1 + ], + [ + 0, + 1, + 1 + ], + [ + 0, + 1, + 0 + ], + [ + 0, + 0, + 0 + ] + ] + ] + }, + "properties": { + } + } + ] +} diff --git a/test/unit/feature2mesh.js b/test/unit/feature2mesh.js index 3691c37f08..504b1f1782 100644 --- a/test/unit/feature2mesh.js +++ b/test/unit/feature2mesh.js @@ -6,6 +6,8 @@ import Feature2Mesh from 'Converter/Feature2Mesh'; const geojson = require('../data/geojson/holes.geojson.json'); const geojson2 = require('../data/geojson/simple.geojson.json'); +const geojson3 = require('../data/geojson/vertical.geojson.json'); + proj4.defs('EPSG:3946', '+proj=lcc +lat_1=45.25 +lat_2=46.75 +lat_0=46 +lon_0=3 +x_0=1700000 +y_0=5200000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'); @@ -29,6 +31,7 @@ function computeAreaOfMesh(mesh) { describe('Feature2Mesh', function () { const parsed = GeoJsonParser.parse(geojson, { in: { crs: 'EPSG:3946' }, out: { crs: 'EPSG:3946', buildExtent: true, mergeFeatures: false, structure: '3d' } }); const parsed2 = GeoJsonParser.parse(geojson2, { in: { crs: 'EPSG:3946' }, out: { crs: 'EPSG:3946', buildExtent: true, mergeFeatures: false, structure: '3d' } }); + const parsed3 = GeoJsonParser.parse(geojson3, { in: { crs: 'EPSG:3946' }, out: { crs: 'EPSG:3946', buildExtent: true, mergeFeatures: false, structure: '3d' } }); it('rect mesh area should match geometry extent', () => parsed.then((collection) => { @@ -43,7 +46,6 @@ describe('Feature2Mesh', function () { it('square mesh area should match geometry extent minus holes', () => parsed.then((collection) => { const mesh = Feature2Mesh.convert()(collection); - const noHoleArea = computeAreaOfMesh(mesh.children[0]); const holeArea = computeAreaOfMesh(mesh.children[1]); const meshWithHoleArea = computeAreaOfMesh(mesh.children[2]); @@ -53,6 +55,26 @@ describe('Feature2Mesh', function () { meshWithHoleArea); })); + it('vertical polygon triangulation', () => + parsed3.then((collection) => { + const mesh = Feature2Mesh.convert()(collection); + const geom = mesh.children[0].geometry; + assert.equal( + geom.index.count, + 6, + ); + })); + + it('vertical polygon triangulation without plan reprojection', () => + parsed3.then((collection) => { + const mesh = Feature2Mesh.convert({ withoutPlanReprojection: 'toto' })(collection); + const geom = mesh.children[0].geometry; + assert.equal( + geom.index.count, + 0, + ); + })); + it('convert points, lines and mesh', () => parsed2.then((collection) => { const mesh = Feature2Mesh.convert()(collection); diff --git a/test/unit/source.js b/test/unit/source.js index 873a9efa63..d5ed7d8f01 100644 --- a/test/unit/source.js +++ b/test/unit/source.js @@ -241,7 +241,7 @@ describe('Sources', function () { layer.whenReady.then(() => { const promise = source.loadData([], layer); promise.then((featureCollection) => { - assert.equal(featureCollection.features[0].vertices.length, 3536); + assert.equal(featureCollection.features[0].vertices.length, 3534); done(); }); });