Skip to content

Commit 0d777ce

Browse files
committed
feature(FeatureGeometryLayer): introduce FeatureMesh, they are added to layer.object3d.
FeatureMesh could be it can dynamically change its geographic projection system.
1 parent faf58be commit 0d777ce

12 files changed

+226
-60
lines changed

src/Converter/Feature2Mesh.js

+76-9
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,84 @@ import Earcut from 'earcut';
33
import { FEATURE_TYPES } from 'Core/Feature';
44
import ReferLayerProperties from 'Layer/ReferencingLayerProperties';
55
import { deprecatedFeature2MeshOptions } from 'Core/Deprecated/Undeprecator';
6+
import Extent from 'Core/Geographic/Extent';
7+
import Crs from 'Core/Geographic/Crs';
8+
import OrientationUtils from 'Utils/OrientationUtils';
9+
import Coordinates from 'Core/Geographic/Coordinates';
10+
11+
const coord = new Coordinates('EPSG:4326', 0, 0, 0);
12+
const dim_ref = new THREE.Vector2();
13+
const dim = new THREE.Vector2();
14+
const extent = new Extent('EPSG:4326', 0, 0, 0, 0);
615

716
const _color = new THREE.Color();
817
const maxValueUint8 = 2 ** 8 - 1;
918
const maxValueUint16 = 2 ** 16 - 1;
1019
const maxValueUint32 = 2 ** 32 - 1;
20+
const crsWGS84 = 'EPSG:4326';
21+
22+
class FeatureMesh extends THREE.Group {
23+
#currentCrs;
24+
#originalCrs;
25+
constructor(meshes, collection) {
26+
super();
27+
this.meshesCollection = new THREE.Group().add(...meshes);
28+
this.meshesCollection.quaternion.copy(collection.quaternion);
29+
this.meshesCollection.position.copy(collection.position);
30+
this.meshesCollection.scale.copy(collection.scale);
31+
this.meshesCollection.updateMatrix();
32+
33+
this.#originalCrs = collection.crs;
34+
this.#currentCrs = this.#originalCrs;
35+
this.extent = collection.extent;
36+
this.place = new THREE.Group();
37+
this.geoid = new THREE.Group();
38+
39+
this.add(this.place.add(this.geoid.add(this.meshesCollection)));
40+
}
41+
42+
as(crs) {
43+
if (this.#currentCrs !== crs) {
44+
this.#currentCrs = crs;
45+
if (crs == this.#originalCrs) {
46+
// reset transformation
47+
this.place.position.set(0, 0, 0);
48+
this.position.set(0, 0, 0);
49+
this.scale.set(1, 1, 1);
50+
this.quaternion.identity();
51+
} else {
52+
// calculate the scale transformation to transform the feature.extent
53+
// to feature.extent.as(crs)
54+
coord.crs = Crs.formatToEPSG(this.#originalCrs);
55+
extent.copy(this.extent).applyMatrix4(this.meshesCollection.matrix);
56+
extent.as(coord.crs, extent);
57+
extent.spatialEuclideanDimensions(dim_ref);
58+
extent.planarDimensions(dim);
59+
if (dim.x && dim.y) {
60+
this.scale.copy(dim_ref).divide(dim).setZ(1);
61+
}
62+
63+
// Position and orientation
64+
// remove original position
65+
this.place.position.copy(this.meshesCollection.position).negate();
66+
67+
// get mesh coordinate
68+
coord.setFromVector3(this.meshesCollection.position);
69+
70+
// get method to calculate orientation
71+
const crsInput = this.#originalCrs == 'EPSG:3857' ? crsWGS84 : this.#originalCrs;
72+
const crs2crs = OrientationUtils.quaternionFromCRSToCRS(crsInput, crs);
73+
// calculate orientation to crs
74+
crs2crs(coord.as(crsWGS84), this.quaternion);
75+
76+
// transform position to crs
77+
coord.as(crs, coord).toVector3(this.position);
78+
}
79+
}
80+
81+
return this;
82+
}
83+
}
1184

1285
function toColor(color) {
1386
if (color) {
@@ -507,18 +580,12 @@ export default {
507580

508581
if (!features || features.length == 0) { return; }
509582

510-
const group = new THREE.Group();
511583
options.GlobalZTrans = collection.center.z;
512584

513-
group.layer = options.layer;
514-
515-
features.forEach(feature => group.add(featureToMesh(feature, options)));
516-
517-
group.quaternion.copy(collection.quaternion);
518-
group.position.copy(collection.position);
519-
group.scale.copy(collection.scale);
585+
const meshes = features.map(feature => featureToMesh(feature, options));
586+
const featureNode = new FeatureMesh(meshes, collection);
520587

521-
return group;
588+
return featureNode;
522589
};
523590
},
524591
};

src/Core/Feature.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ export class FeatureCollection extends THREE.Object3D {
362362
constructor(options) {
363363
super();
364364
this.isFeatureCollection = true;
365-
this.crs = CRS.formatToEPSG(options.crs);
365+
this.crs = CRS.formatToEPSG(options.accurate || !options.source?.crs ? options.crs : options.source.crs);
366366
this.features = [];
367367
this.mergeFeatures = options.mergeFeatures === undefined ? true : options.mergeFeatures;
368368
this.size = options.structure == '3d' ? 3 : 2;

src/Core/TileMesh.js

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class TileMesh extends THREE.Mesh {
4545
this.domElements = {};
4646

4747
this.geoidHeight = 0;
48+
49+
this.link = [];
4850
}
4951

5052
/**

src/Layer/FeatureGeometryLayer.js

+30-7
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,45 @@ class FeatureGeometryLayer extends GeometryLayer {
2222
* @param {string} id - The id of the layer, that should be unique. It is
2323
* not mandatory, but an error will be emitted if this layer is added a
2424
* {@link View} that already has a layer going by that id.
25-
* @param {Object} [config] - Optional configuration, all elements in it
25+
* @param {Object} [options] - Optional configuration, all elements in it
2626
* will be merged as is in the layer.
2727
* @param {function} [options.batchId] - optional function to create batchId attribute.
2828
* It is passed the feature property and the feature index.
2929
* As the batchId is using an unsigned int structure on 32 bits, the batchId could be between 0 and 4,294,967,295.
30-
* @param {THREE.Object3D} [config.object3d=new THREE.Group()] root object3d layer.
30+
* @param {THREE.Object3D} [options.object3d=new THREE.Group()] root object3d layer.
31+
* @param {function} [options.onMeshCreated] this callback is called when the mesh is created. The callback parameters are the
32+
* `mesh` and the `context`.
33+
* @param {boolean} [options.accurate=TRUE] If `accurate` is `true`, data are re-projected with maximum geographical accuracy.
34+
* With `true`, `proj4` is used to transform data source.
3135
*
36+
* If `accurate` is `false`, re-projecting is faster but less accurate.
37+
* With `false`, an affine transformation is used to transform data source.
38+
* This method is an approximation. The error increases with the extent
39+
* dimension of the object or queries.
40+
*
41+
* For example :
42+
* * for a **100** meter dimension, there's a difference of **0.001** meter with the accurate method
43+
* * for a **500** meter dimension, there's a difference of **0.05** meter with the accurate method
44+
* * for a **20000** meter dimension, there's a difference of **40** meter with the accurate method
45+
*
46+
* **WARNING** If the source is `VectorTilesSource` then `accurate` is always false.
3247
*/
33-
constructor(id, config = {}) {
34-
config.update = FeatureProcessing.update;
35-
config.convert = Feature2Mesh.convert({
36-
batchId: config.batchId,
48+
constructor(id, options = {}) {
49+
options.update = FeatureProcessing.update;
50+
options.convert = Feature2Mesh.convert({
51+
batchId: options.batchId,
3752
},
3853
);
39-
super(id, config.object3d || new Group(), config);
54+
super(id, options.object3d || new Group(), options);
4055
this.isFeatureGeometryLayer = true;
56+
this.accurate = options.accurate ?? true;
57+
this.buildExtent = !this.accurate;
58+
}
59+
60+
preUpdate(context, sources) {
61+
if (sources.has(this.parent)) {
62+
this.object3d.clear();
63+
}
4164
}
4265
}
4366

src/Layer/GeometryLayer.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,7 @@ class GeometryLayer extends Layer {
207207
* specified coordinates.
208208
*/
209209
pickObjectsAt(view, coordinates, radius = this.options.defaultPickingRadius, target = []) {
210-
const object3d = this.parent ? this.parent.object3d : this.object3d;
211-
return Picking.pickObjectsAt(view, coordinates, radius, object3d, target, this.threejsLayer);
210+
return Picking.pickObjectsAt(view, coordinates, radius, this.object3d, target, this.threejsLayer);
212211
}
213212
}
214213

src/Layer/OrientedImageLayer.js

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class OrientedImageLayer extends GeometryLayer {
128128

129129
this.mergeFeatures = false;
130130
this.filteringExtent = false;
131+
this.accurate = true;
131132
const options = { out: this };
132133

133134
// panos is an array of feature point, representing many panoramics.

src/Process/FeatureProcessing.js

+37-31
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import * as THREE from 'three';
21
import LayerUpdateState from 'Layer/LayerUpdateState';
32
import ObjectRemovalHelper from 'Process/ObjectRemovalHelper';
43
import handlingError from 'Process/handlerNodeError';
54
import Coordinates from 'Core/Geographic/Coordinates';
65

76
const coord = new Coordinates('EPSG:4326', 0, 0, 0);
87

8+
function geoidLayerIsVisible(layer) {
9+
return layer.parent?.attachedLayers.filter(l => l.isGeoidLayer)[0]?.visible;
10+
}
11+
912
export default {
1013
update(context, layer, node) {
1114
if (!node.parent && node.children.length) {
@@ -20,15 +23,17 @@ export default {
2023
if (node.layerUpdateState[layer.id] === undefined) {
2124
node.layerUpdateState[layer.id] = new LayerUpdateState();
2225
} else if (!node.layerUpdateState[layer.id].canTryUpdate()) {
26+
// toggle visibility features
27+
node.link.forEach((f) => {
28+
if (f.layer?.id == layer.id) {
29+
f.layer.object3d.add(f);
30+
f.geoid.position.z = geoidLayerIsVisible(layer) ? node.geoidHeight : 0;
31+
f.geoid.updateMatrixWorld();
32+
}
33+
});
2334
return;
2435
}
2536

26-
const features = node.children.filter(n => n.layer == layer);
27-
28-
if (features.length > 0) {
29-
return features;
30-
}
31-
3237
const extentsDestination = node.getExtentsByProjection(layer.source.crs) || [node.extent];
3338

3439
const zoomDest = extentsDestination[0].zoom;
@@ -54,31 +59,32 @@ export default {
5459
requester: node,
5560
};
5661

57-
return context.scheduler.execute(command).then((result) => {
58-
// if request return empty json, WFSProvider.getFeatures return undefined
59-
result = result[0];
60-
if (result) {
61-
// call onMeshCreated callback if needed
62-
if (layer.onMeshCreated) {
63-
layer.onMeshCreated(result);
64-
}
65-
node.layerUpdateState[layer.id].success();
66-
if (!node.parent) {
67-
ObjectRemovalHelper.removeChildrenAndCleanupRecursively(layer, result);
68-
return;
62+
return context.scheduler.execute(command).then((featureMeshes) => {
63+
node.layerUpdateState[layer.id].noMoreUpdatePossible();
64+
65+
featureMeshes.forEach((featureMesh) => {
66+
if (featureMesh) {
67+
featureMesh.as(context.view.referenceCrs);
68+
featureMesh.geoid.position.z = geoidLayerIsVisible(layer) ? node.geoidHeight : 0;
69+
featureMesh.updateMatrixWorld();
70+
71+
if (layer.onMeshCreated) {
72+
layer.onMeshCreated(featureMesh, context);
73+
}
74+
75+
if (!node.parent) {
76+
// TODO: Clean cache needs a refactory, because it isn't really efficient and used
77+
ObjectRemovalHelper.removeChildrenAndCleanupRecursively(layer, featureMesh);
78+
} else {
79+
layer.object3d.add(featureMesh);
80+
node.link.push(featureMesh);
81+
}
82+
featureMesh.layer = layer;
83+
} else {
84+
// TODO: verify if it's possible the featureMesh is undefined.
85+
node.layerUpdateState[layer.id].failure(1, true);
6986
}
70-
// remove old group layer
71-
node.remove(...node.children.filter(c => c.layer && c.layer.id == layer.id));
72-
const group = new THREE.Group();
73-
group.layer = layer;
74-
group.matrixWorld.copy(node.matrixWorld).invert();
75-
group.matrixWorld.decompose(group.position, group.quaternion, group.scale);
76-
group.position.z += node.geoidHeight;
77-
node.add(group.add(result));
78-
group.updateMatrixWorld(true);
79-
} else {
80-
node.layerUpdateState[layer.id].failure(1, true);
81-
}
87+
});
8288
},
8389
err => handlingError(err, node, layer, node.level, context.view));
8490
},

src/Process/ObjectRemovalHelper.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ export default {
6868
* @return {Array} an array of removed Object3D from obj (not including the recursive removals)
6969
*/
7070
removeChildrenAndCleanupRecursively(layer, obj) {
71-
const toRemove = obj.children.filter(c => (c.layer && c.layer.id) === layer.id);
71+
let toRemove = obj.children.filter(c => (c.layer && c.layer.id) === layer.id);
72+
if (obj.link) {
73+
toRemove = toRemove.concat(obj.link);
74+
}
7275
for (const c of toRemove) {
7376
this.removeChildrenAndCleanupRecursively(layer, c);
7477
}

src/Source/VectorTilesSource.js

+4
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ class VectorTilesSource extends TMSSource {
130130
onLayerAdded(options) {
131131
super.onLayerAdded(options);
132132
if (options.out.style) {
133+
if (options.out.isFeatureGeometryLayer && options.out.accurate) {
134+
console.warn('With VectorTilesSource and FeatureGeometryLayer, the accurate option is always false');
135+
options.out.accurate = false;
136+
}
133137
const keys = Object.keys(this.styles);
134138

135139
keys.forEach((k) => { this.styles[k].parent = options.out.style; });

test/unit/dataSourceProvider.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ describe('Provide in Sources', function () {
226226

227227
featureLayer.update(context, featureLayer, tile);
228228
DataSourceProvider.executeCommand(context.scheduler.commands[0]).then((features) => {
229-
assert.equal(features[0].children.length, 4);
229+
assert.equal(features[0].meshesCollection.children.length, 4);
230230
done();
231231
});
232232
});
@@ -247,10 +247,10 @@ describe('Provide in Sources', function () {
247247
featureLayer.source.onLayerAdded({ out: featureLayer });
248248
featureLayer.update(context, featureLayer, tile);
249249
DataSourceProvider.executeCommand(context.scheduler.commands[0]).then((features) => {
250-
assert.ok(features[0].children[0].isMesh);
251-
assert.ok(features[0].children[1].isPoints);
252-
assert.equal(features[0].children[0].children.length, 0);
253-
assert.equal(features[0].children[1].children.length, 0);
250+
assert.ok(features[0].meshesCollection.children[0].isMesh);
251+
assert.ok(features[0].meshesCollection.children[1].isPoints);
252+
assert.equal(features[0].meshesCollection.children[0].children.length, 0);
253+
assert.equal(features[0].meshesCollection.children[1].children.length, 0);
254254
assert.equal(featureCountByCb, 2);
255255
done();
256256
});

test/unit/feature2mesh.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('Feature2Mesh', function () {
3232

3333
it('rect mesh area should match geometry extent', () =>
3434
parsed.then((collection) => {
35-
const mesh = Feature2Mesh.convert()(collection);
35+
const mesh = Feature2Mesh.convert()(collection).meshesCollection;
3636
const extentSize = collection.extent.planarDimensions();
3737

3838
assert.equal(
@@ -42,7 +42,7 @@ describe('Feature2Mesh', function () {
4242

4343
it('square mesh area should match geometry extent minus holes', () =>
4444
parsed.then((collection) => {
45-
const mesh = Feature2Mesh.convert()(collection);
45+
const mesh = Feature2Mesh.convert()(collection).meshesCollection;
4646

4747
const noHoleArea = computeAreaOfMesh(mesh.children[0]);
4848
const holeArea = computeAreaOfMesh(mesh.children[1]);
@@ -55,7 +55,7 @@ describe('Feature2Mesh', function () {
5555

5656
it('convert points, lines and mesh', () =>
5757
parsed2.then((collection) => {
58-
const mesh = Feature2Mesh.convert()(collection);
58+
const mesh = Feature2Mesh.convert()(collection).meshesCollection;
5959
assert.equal(mesh.children[0].type, 'Points');
6060
assert.equal(mesh.children[1].type, 'Line');
6161
assert.equal(mesh.children[2].type, 'Mesh');

0 commit comments

Comments
 (0)