diff --git a/examples/source_stream_wfs_25d.html b/examples/source_stream_wfs_25d.html index 978a7a7d03..167c5a5f71 100644 --- a/examples/source_stream_wfs_25d.html +++ b/examples/source_stream_wfs_25d.html @@ -235,8 +235,10 @@ altitude: altitudeBuildings, extrude: extrudeBuildings }), onMeshCreated: function scaleZ(mesh) { - mesh.scale.z = 0.01; - meshes.push(mesh); + mesh.children.forEach(c => { + c.scale.z = 0.01; + meshes.push(c); + }) }, filter: acceptFeature, overrideAltitudeInToZero: true, diff --git a/examples/source_stream_wfs_3d.html b/examples/source_stream_wfs_3d.html index c630264bad..8187bde1ff 100644 --- a/examples/source_stream_wfs_3d.html +++ b/examples/source_stream_wfs_3d.html @@ -186,8 +186,10 @@ altitude: altitudeBuildings, extrude: extrudeBuildings }), onMeshCreated: function scaleZ(mesh) { - mesh.scale.z = 0.01; - meshes.push(mesh); + mesh.children.forEach(c => { + c.scale.z = 0.01; + meshes.push(c); + }) }, filter: acceptFeature, overrideAltitudeInToZero: true, diff --git a/src/Controls/StreetControls.js b/src/Controls/StreetControls.js index 38b9e78069..48ad942ff2 100644 --- a/src/Controls/StreetControls.js +++ b/src/Controls/StreetControls.js @@ -31,6 +31,8 @@ function updateSurfaces(surfaces, position, norm) { // vector use in the pick method const target = new THREE.Vector3(); +const normal = new THREE.Vector3(); +const normalMatrix = new THREE.Matrix3(); const up = new THREE.Vector3(); const startQuaternion = new THREE.Quaternion(); @@ -45,7 +47,9 @@ function pick(event, view, buildingsLayer, pickGround = () => {}, pickObject = ( // to detect pick on building, compare first picked building distance to ground distance if (buildings.length && buildings[0].distance < distanceToGround) { // pick buildings // callback - pickObject(buildings[0].point, buildings[0].face.normal); + normalMatrix.getNormalMatrix(buildings[0].object.matrixWorld); + normal.copy(buildings[0].face.normal).applyNormalMatrix(normalMatrix); + pickObject(buildings[0].point, normal); } else if (view.tileLayer) { const far = view.camera.camera3D.far * 0.95; if (distanceToGround < far) { diff --git a/src/Converter/Feature2Mesh.js b/src/Converter/Feature2Mesh.js index 634de98b92..deaf261360 100644 --- a/src/Converter/Feature2Mesh.js +++ b/src/Converter/Feature2Mesh.js @@ -428,29 +428,6 @@ function featureToMesh(feature, options) { return mesh; } -function featuresToThree(features, options) { - if (!features || features.length == 0) { return; } - - if (features.length == 1) { - coord.crs = features[0].crs; - coord.setFromValues(0, 0, 0); - return featureToMesh(features[0], options); - } - - const group = new THREE.Group(); - group.minAltitude = Infinity; - - for (const feature of features) { - coord.crs = feature.crs; - coord.setFromValues(0, 0, 0); - const mesh = featureToMesh(feature, options); - group.add(mesh); - group.minAltitude = Math.min(mesh.minAltitude, group.minAltitude); - } - - return group; -} - /** * @module Feature2Mesh */ @@ -505,7 +482,19 @@ export default { return function _convert(collection) { if (!collection) { return; } - return featuresToThree(collection.features, options); + const features = collection.features; + + if (!features || features.length == 0) { return; } + + const group = new THREE.Group(); + options.GlobalZTrans = collection.center.z; + + features.forEach(feature => group.add(featureToMesh(feature, options))); + + group.quaternion.copy(collection.quaternion); + group.position.copy(collection.position); + + return group; }; }, }; diff --git a/src/Core/Feature.js b/src/Core/Feature.js index dcc79cdfa3..8241b21602 100644 --- a/src/Core/Feature.js +++ b/src/Core/Feature.js @@ -117,6 +117,8 @@ export class FeatureGeometry { pushCoordinates(coordIn, feature) { coordIn.as(feature.crs, coordOut); + feature.transformToLocalSystem(coordOut); + if (feature.normals) { coordOut.geodesicNormal.toArray(feature.normals, feature._pos); } @@ -131,6 +133,7 @@ export class FeatureGeometry { /** * Push new values coordinates in vertices buffer. * No geographical conversion is made or the normal doesn't stored. + * No local transformation is made on coordinates. * * @param {Feature} feature - the feature containing the geometry * @param {number} long The longitude coordinate. @@ -222,6 +225,7 @@ class Feature { this.crs = collection.crs; this.size = collection.size; this.normals = collection.size == 3 ? [] : undefined; + this.transformToLocalSystem = collection.transformToLocalSystem.bind(collection); if (collection.extent) { // this.crs is final crs projection, is out projection. // If the extent crs is the same then we use output coordinate (coordOut) to expand it. @@ -261,9 +265,20 @@ class Feature { export default Feature; +const doNothing = () => {}; + +const transformToLocalSystem3D = (coord, collection) => { + coord.geodesicNormal.applyNormalMatrix(collection.normalMatrixInverse); + return coord.applyMatrix4(collection.matrixWorldInverse); +}; + +const transformToLocalSystem2D = (coord, collection) => coord.applyMatrix4(collection.matrixWorldInverse); +const axisZ = new THREE.Vector3(0, 0, 1); +const alignYtoEast = new THREE.Quaternion(); /** * An object regrouping a list of [features]{@link Feature} and the extent of this collection. * **Warning**, the data (`extent` or `Coordinates`) can be stored in a local system. + * The local system center is the `center` property. * To use `Feature` vertices or `FeatureCollection/Feature` extent in FeatureCollection.crs projection, * it's necessary to transform `Coordinates` or `Extent` by `FeatureCollection.matrixWorld`. * @@ -301,6 +316,8 @@ export default Feature; * https://alastaira.wordpress.com/2011/07/06/converting-tms-tile-coordinates-to-googlebingosm-tile-coordinates} * for more informations. * @property {THREE.Matrix4} matrixWorldInverse - The matrix world inverse. + * @property {Coordinates} center - The local center coordinates in `EPSG:4326`. + * The local system is centred in this center. * */ @@ -321,6 +338,55 @@ export class FeatureCollection extends THREE.Object3D { this.style = options.style; this.isInverted = false; this.matrixWorldInverse = new THREE.Matrix4(); + this.center = new Coordinates('EPSG:4326', 0, 0); + + if (this.size == 2) { + this._setLocalSystem = (center) => { + // set local system center + center.as('EPSG:4326', this.center); + + // set position to local system center + this.position.copy(center); + this.updateMatrixWorld(); + this._setLocalSystem = doNothing; + }; + this._transformToLocalSystem = transformToLocalSystem2D; + } else { + this._setLocalSystem = (center) => { + // set local system center + center.as('EPSG:4326', this.center); + + if (this.crs == 'EPSG:4978') { + // align Z axe to geodesic normal. + this.quaternion.setFromUnitVectors(axisZ, center.geodesicNormal); + // align Y axe to East + alignYtoEast.setFromAxisAngle(axisZ, THREE.MathUtils.degToRad(90 + this.center.longitude)); + this.quaternion.multiply(alignYtoEast); + } + + // set position to local system center + this.position.copy(center); + this.updateMatrixWorld(); + this.normalMatrix.getNormalMatrix(this.matrix); + this.normalMatrixInverse = new THREE.Matrix3().copy(this.normalMatrix).invert(); + + this._setLocalSystem = doNothing; + }; + this._transformToLocalSystem = transformToLocalSystem3D; + } + } + + /** + * Apply the matrix World inverse on the coordinates. + * This method is used when the coordinates is pushed + * to transform it in local system. + * + * @param {Coordinates} coordinates The coordinates + * @returns {Coordinates} The coordinates in local system + */ + transformToLocalSystem(coordinates) { + this._setLocalSystem(coordinates); + return this._transformToLocalSystem(coordinates, this); } /** diff --git a/src/Layer/OrientedImageLayer.js b/src/Layer/OrientedImageLayer.js index 06902851f1..00e42ba457 100644 --- a/src/Layer/OrientedImageLayer.js +++ b/src/Layer/OrientedImageLayer.js @@ -147,7 +147,7 @@ class OrientedImageLayer extends GeometryLayer { for (const pano of this.panos) { // set position coord.crs = pano.crs; - coord.setFromArray(pano.vertices); + coord.setFromArray(pano.vertices).applyMatrix4(orientation.matrix); pano.position = coord.toVector3(); // set quaternion diff --git a/src/Process/FeatureProcessing.js b/src/Process/FeatureProcessing.js index 1c5fd18e02..9c3c2f0609 100644 --- a/src/Process/FeatureProcessing.js +++ b/src/Process/FeatureProcessing.js @@ -5,14 +5,6 @@ import handlingError from 'Process/handlerNodeError'; import Coordinates from 'Core/Geographic/Coordinates'; const coord = new Coordinates('EPSG:4326', 0, 0, 0); -const mat4 = new THREE.Matrix4(); - -function applyMatrix4(obj, mat4) { - if (obj.geometry) { - obj.geometry.applyMatrix4(mat4); - } - obj.children.forEach(c => applyMatrix4(c, mat4)); -} function assignLayer(object, layer) { if (object) { @@ -89,7 +81,6 @@ export default { // if request return empty json, WFSProvider.getFeatures return undefined result = result[0]; if (result) { - const isApplied = !result.layer; assignLayer(result, layer); // call onMeshCreated callback if needed if (layer.onMeshCreated) { @@ -100,22 +91,14 @@ export default { ObjectRemovalHelper.removeChildrenAndCleanupRecursively(layer, result); return; } - // We don't use node.matrixWorld here, because feature coordinates are - // expressed in crs coordinates (which may be different than world coordinates, - // if node's layer is attached to an Object with a non-identity transformation) - if (isApplied) { - // NOTE: now data source provider use cache on Mesh - // TODO move transform in feature2Mesh - mat4.copy(node.matrixWorld).invert().elements[14] -= result.minAltitude; - applyMatrix4(result, mat4); - } - - if (result.minAltitude) { - result.position.z = result.minAltitude; - } - result.layer = layer; - node.add(result); - node.updateMatrixWorld(); + // remove old group layer + node.remove(...node.children.filter(c => c.layer && c.layer.id == layer.id)); + const group = new THREE.Group(); + group.layer = layer; + group.matrixWorld.copy(node.matrixWorld).invert(); + group.matrixWorld.decompose(group.position, group.quaternion, group.scale); + node.add(group.add(result)); + group.updateMatrixWorld(true); } else { node.layerUpdateState[layer.id].failure(1, true); } diff --git a/test/unit/feature.js b/test/unit/feature.js index d2d60ca089..eb3cfb0b91 100644 --- a/test/unit/feature.js +++ b/test/unit/feature.js @@ -12,16 +12,18 @@ describe('Feature', function () { const coord = new Coordinates('EPSG:4326', 0, 0, 0); it('Should instance Features', function () { - const featurePoint = new Feature(FEATURE_TYPES.POINT, 'EPSG:4326'); - const featureLine = new Feature(FEATURE_TYPES.LINE, 'EPSG:4326'); - const featurePolygon = new Feature(FEATURE_TYPES.POLYGON, 'EPSG:4326'); + const collection = new FeatureCollection(options_A); + const featurePoint = new Feature(FEATURE_TYPES.POINT, collection); + const featureLine = new Feature(FEATURE_TYPES.LINE, collection); + const featurePolygon = new Feature(FEATURE_TYPES.POLYGON, collection); assert.equal(featurePoint.type, FEATURE_TYPES.POINT); assert.equal(featureLine.type, FEATURE_TYPES.LINE); assert.equal(featurePolygon.type, FEATURE_TYPES.POLYGON); }); it('Should bind FeatureGeometry', function () { - const featureLine = new Feature(FEATURE_TYPES.LINE, 'EPSG:4326'); + const collection = new FeatureCollection(options_A); + const featureLine = new Feature(FEATURE_TYPES.LINE, collection); featureLine.bindNewGeometry(); assert.equal(featureLine.geometryCount, 1); }); @@ -56,6 +58,8 @@ describe('Feature', function () { featureLine.updateExtent(geometry); + collection_A.updateMatrix(); + featureLine.extent.applyMatrix4(collection_A.matrix); assert.equal(featureLine.extent.south, -1118889.9748579601); assert.equal(featureLine.vertices.length, geometry.indices[0].count * featureLine.size); assert.equal(featureLine.vertices.length, featureLine.normals.length); diff --git a/test/unit/featureUtils.js b/test/unit/featureUtils.js index 988db3c055..5789d76271 100644 --- a/test/unit/featureUtils.js +++ b/test/unit/featureUtils.js @@ -15,10 +15,11 @@ describe('FeaturesUtils', function () { })); it('should correctly compute extent geojson', () => promise.then((collection) => { - assert.equal(collection.extent.west, 0.30798339284956455); - assert.equal(collection.extent.east, 2.4722900334745646); - assert.equal(collection.extent.south, 42.91620643817353); - assert.equal(collection.extent.north, 43.72744458647463); + const extent = collection.extent.clone().applyMatrix4(collection.matrix); + assert.equal(extent.west, 0.30798339284956455); + assert.equal(extent.east, 2.4722900334745646); + assert.equal(extent.south, 42.91620643817353); + assert.equal(extent.north, 43.72744458647463); })); it('should correctly filter point', () => promise.then((collection) => { diff --git a/test/unit/geojson.js b/test/unit/geojson.js index 961295af37..baa17fdaab 100644 --- a/test/unit/geojson.js +++ b/test/unit/geojson.js @@ -15,14 +15,14 @@ function parse(geojson) { } describe('GeoJsonParser', function () { - it('should set all z coordinates to 1', () => + it('should set all z coordinates to 0', () => parse(holes).then((collection) => { - assert.ok(collection.features[0].vertices.every((v, i) => i == 0 || ((i + 1) % 3) != 0 || v == 0)); + assert.ok(collection.features[0].vertices.every((v, i) => ((i + 1) % 3) != 0 || (v + collection.position.z) == 0)); })); it('should respect all z coordinates', () => parse(gpx).then((collection) => { - assert.ok(collection.features[0].vertices.every((v, i) => i == 0 || ((i + 1) % 3) != 0 || v != 0)); + assert.ok(collection.features[0].vertices.every((v, i) => ((i + 1) % 3) != 0 || (v + collection.position.z) != 0)); })); it('should return an empty collection', () =>