import { Layer } from "./Layer";
import { OGC3DTile } from "@jdultra/threedtiles/dist/threedtiles.min.js";
import * as THREE from 'three';
import { TileLoader } from '@jdultra/threedtiles/dist/threedtiles.min.js';
import { OBB } from '@jdultra/threedtiles/dist/threedtiles.min.js';
import { llhToCartesianFastSFCT } from '../GeoUtils.js';
const tileLoaders = [];
const cartesianLocation = new THREE.Vector3();
const Up = new THREE.Vector3();
const East = new THREE.Vector3();
const North = new THREE.Vector3();
const globalNorth = new THREE.Vector3(0,0,1);
const quaternionToEarthNormalOrientation = new THREE.Quaternion();
const quaternionSelfRotation = new THREE.Quaternion();
const rotationMatrix = new THREE.Matrix4();
const rotation = new THREE.Euler(0,0,0, "ZYX");
/**
* A layer for loading a OGC3DTiles tileset.
* @class
* @extends Layer
*/
class OGC3DTilesLayer extends Layer {
/**
*
* @param {Object} properties
* @param {String|Number} properties.id layer id should be unique
* @param {String} properties.name the name can be anything you want and is intended for labeling
* @param {String} properties.url url of the root tileset.json
* @param {Boolean} [properties.displayCopyright = false] (optional) display copyright information when present in tiles by concatenating all copyright info for all displayed tiles
* @param {Boolean} [properties.displayErrors = false] (optional) display loading errors
* @param {Boolean} [properties.proxy = undefined] (optional) the url to a proxy service. Instead of fetching tiles via a GET request, a POST will be sent to the proxy url with the real tile address in the body of the request.
* @param {Boolean} [properties.queryParams = undefined] (optional) path params to add to individual tile urls (starts with "?").
* @param {number} [scaleX = 1] - scale on X axes.
* @param {number} [scaleY = 1] - scale on Y axes. defaults to the scaleX property if defined.
* @param {number} [scaleZ = 1] - scale on Z axes. defaults to the scaleX property if defined.
* @param {number} [yaw = 0] - Yaw angle in degrees. (0 means local z axis points north ccw rotation)
* @param {number} [pitch = 0] - Pitch angle in degrees (0 means the x-z plane alligns with the horizon )
* @param {number} [roll = 0] - Roll angle in degrees. (ccw rotation about the local z axis)
* @param {Number} [properties.geometricErrorMultiplier = 1] (optional) between 0 and infinity, defaults to 1. controls the level of detail.
* @param {Number} [properties.longitude = 0] (optional) longitude of the model's center point in degrees.
* @param {Number} [properties.latitude = 0] (optional) latitude of the model's center point in degrees.
* @param {Number} [properties.height = 0] (optional) height in meters above sea level.
* @param {Boolean} [properties.loadOutsideView = false] (optional) if true, will load tiles outside the view at the lowest possible LOD.
* @param {Boolean} [properties.selectable = false] (optional) if true, the tileset can be selected.
* @param {Number[]} [properties.bounds=[-180, -90, 180, 90]] min longitude, min latitude, max longitude, max latitude in degrees
* @param {Boolean} [properties.visible = true] layer will be rendered if true (true by default)
* @param {String} [properties.loadingStrategy = "INCREMENTAL"] loading strategy, "INCREMENTAL" (default) or "IMMEDIATE". "IMMEDIATE" mode loads only the ideal LOD while "INCREMENTAL" loads intermediate LODs.
* @param {Function} [properties.updateCallback = undefined] A callback called on every tileset update with a stats object indicating number of tiles loaded/visualized, max loaded LOD, and percentage of the tileset loaded
*
*/
constructor(properties) {
if (!properties) {
throw "Bad instanciation, OGC3DTilesLayer requires properties."
}
super(properties);
this.isOGC3DTilesLayer = true;
const self = this;
self.properties = properties;
self.displayCopyright = properties.displayCopyright;
self.displayErrors = properties.displayErrors;
self.proxy = properties.proxy;
self.queryParams = properties.queryParams;
this.move(properties.longitude, properties.latitude, properties.height, properties.yaw, properties.pitch, properties.roll, properties.scaleX, properties.scaleY, properties.scaleZ);
this.geometricErrorMultiplier = !!properties.geometricErrorMultiplier ? properties.geometricErrorMultiplier : 1.0;
this.loadingStrategy = !!properties.loadingStrategy ? properties.loadingStrategy : "INCREMENTAL";
this.updateCallback = !!properties.updateCallback ? properties.updateCallback : undefined;
this.url = properties.url;
this.loadOutsideView = !!properties.loadOutsideView ? properties.loadOutsideView : false;
this.selected = false;
this.selectable = !!properties.selectable;
}
getCenter(sfct) {
sfct.set(this._longitude, this._latitude, this._height);
}
getRadius() {
return this.bounds.min.distanceTo(this.bounds.max);
}
getBaseHeight() {
const bounds = this.tileset.boundingVolume;
if (bounds) {
if (bounds instanceof OBB) {
return - bounds.halfDepth;
} else if (bounds instanceof THREE.Sphere) {
return - bounds.radius;
}
}
return 0;
}
generateControlShapes(tileset) {
if (tileset.json.boundingVolume.region) {
} else if (tileset.json.boundingVolume.box) {
} else if (tileset.json.boundingVolume.sphere) {
}
if (tileset.boundingVolume instanceof OBB) {
// box
// TODO curved edges
const shape = new THREE.Shape();
shape.moveTo(tileset.boundingVolume.center.x + tileset.boundingVolume.halfWidth + tileset.boundingVolume.halfWidth * 0.1, tileset.boundingVolume.center.y + tileset.boundingVolume.halfHeight + tileset.boundingVolume.halfWidth * 0.1);
shape.lineTo(tileset.boundingVolume.center.x + tileset.boundingVolume.halfWidth + tileset.boundingVolume.halfWidth * 0.1, tileset.boundingVolume.center.y - tileset.boundingVolume.halfHeight - tileset.boundingVolume.halfWidth * 0.1);
shape.lineTo(tileset.boundingVolume.center.x - tileset.boundingVolume.halfWidth - tileset.boundingVolume.halfWidth * 0.1, tileset.boundingVolume.center.y - tileset.boundingVolume.halfHeight - tileset.boundingVolume.halfWidth * 0.1);
shape.lineTo(tileset.boundingVolume.center.x - tileset.boundingVolume.halfWidth - tileset.boundingVolume.halfWidth * 0.1, tileset.boundingVolume.center.y + tileset.boundingVolume.halfHeight + tileset.boundingVolume.halfWidth * 0.1);
shape.lineTo(tileset.boundingVolume.center.x + tileset.boundingVolume.halfWidth + tileset.boundingVolume.halfWidth * 0.1, tileset.boundingVolume.center.y + tileset.boundingVolume.halfHeight + tileset.boundingVolume.halfWidth * 0.1);
const hole = new THREE.Shape();
hole.moveTo(tileset.boundingVolume.center.x + tileset.boundingVolume.halfWidth + tileset.boundingVolume.halfWidth * 0.0, tileset.boundingVolume.center.y + tileset.boundingVolume.halfHeight + tileset.boundingVolume.halfWidth * 0.0);
hole.lineTo(tileset.boundingVolume.center.x + tileset.boundingVolume.halfWidth + tileset.boundingVolume.halfWidth * 0.0, tileset.boundingVolume.center.y - tileset.boundingVolume.halfHeight - tileset.boundingVolume.halfWidth * 0.0);
hole.lineTo(tileset.boundingVolume.center.x - tileset.boundingVolume.halfWidth - tileset.boundingVolume.halfWidth * 0.0, tileset.boundingVolume.center.y - tileset.boundingVolume.halfHeight - tileset.boundingVolume.halfWidth * 0.0);
hole.lineTo(tileset.boundingVolume.center.x - tileset.boundingVolume.halfWidth - tileset.boundingVolume.halfWidth * 0.0, tileset.boundingVolume.center.y + tileset.boundingVolume.halfHeight + tileset.boundingVolume.halfWidth * 0.0);
hole.lineTo(tileset.boundingVolume.center.x + tileset.boundingVolume.halfWidth + tileset.boundingVolume.halfWidth * 0.0, tileset.boundingVolume.center.y + tileset.boundingVolume.halfHeight + tileset.boundingVolume.halfWidth * 0.0);
shape.holes.push(hole);
const geometry = new THREE.ShapeGeometry(shape);
geometry.translate(0, 0, -tileset.boundingVolume.halfDepth);
const matrix = new THREE.Matrix4();
matrix.setFromMatrix3(tileset.boundingVolume.matrixToOBBCoordinateSystem);
geometry.applyMatrix4(matrix);
geometry.translate(tileset.boundingVolume.center.x, tileset.boundingVolume.center.y, tileset.boundingVolume.center.z);
this.selectionMesh = new THREE.Mesh(geometry,
new THREE.MeshBasicMaterial(
{
color: 0xFFB24E,
transparent: true,
opacity: 0.5,
depthWrite: true,
side: THREE.DoubleSide,
depthTest: true
}
)
);
const geometry2 = new THREE.BoxGeometry(tileset.boundingVolume.halfWidth * 2, tileset.boundingVolume.halfHeight * 2, tileset.boundingVolume.halfDepth * 2);
geometry2.applyMatrix4(matrix);
geometry2.translate(tileset.boundingVolume.center.x, tileset.boundingVolume.center.y, tileset.boundingVolume.center.z);
this.boundingMesh = new THREE.Mesh(geometry2, new THREE.MeshBasicMaterial({
color: 0xFFB24E,
transparent: true,
opacity: 0.3,
depthWrite: true,
side: THREE.DoubleSide,
depthTest: true
}));
this.boundingMeshOutline = new THREE.BoxHelper(this.boundingMesh, 0xFFB24E);
} else if (tileset.boundingVolume instanceof THREE.Sphere) {
//sphere
const geometry = new THREE.SphereGeometry(tileset.boundingVolume.radius, 32, 16)
geometry.translate(tileset.boundingVolume.center.x, tileset.boundingVolume.center.y, tileset.boundingVolume.center.z);
this.boundingMesh = new THREE.Mesh(geometry,
new THREE.MeshBasicMaterial(
{
color: 0x04E7FF,
transparent: true,
opacity: 0.3,
depthWrite: true,
side: THREE.DoubleSide,
depthTest: true
}
));
this.selectionMesh = this.boundingMesh.clone();
} else if (tile.boundingVolume instanceof THREE.Box3) {
// Region
// Region not supported
console.error("Region bounding volume not supported");
return;
}
this.boundingMesh.layer = this;
this.update();
}
_setMap(map) {
const self = this;
var tileLoader = !!self.properties.tileLoader ? self.properties.tileLoader : new TileLoader({
renderer: map.renderer,
maxCachedItems: 0,
meshCallback: (mesh, geometricError) => {
//mesh.material = new THREE.MeshLambertMaterial({color: new THREE.Color("rgb("+(Math.floor(Math.random()*256))+", "+(Math.floor(Math.random()*256))+", "+(Math.floor(Math.random()*256))+")")});
if (mesh.material.isMeshBasicMaterial) {
const newMat = new THREE.MeshStandardMaterial();
newMat.map = mesh.material.map;
mesh.material = newMat;
}
/* mesh.material.color.copy(new THREE.Color("rgb("+(Math.floor(Math.random()*256))+", "+(Math.floor(Math.random()*256))+", "+(Math.floor(Math.random()*256))+"))+"));
mesh.material.needsUpdate = true */
if (mesh.material.map) {
mesh.material.map.colorSpace = THREE.LinearSRGBColorSpace;
}
mesh.material.wireframe = false;
mesh.material.side = THREE.DoubleSide;
if (!mesh.geometry.getAttribute('normal')) {
mesh.geometry.computeVertexNormals();
}
if (map.csm) {
mesh.material.side = THREE.FrontSide;
mesh.castShadow = true
mesh.receiveShadow = true;
mesh.parent.castShadow = true
mesh.parent.receiveShadow = true;
mesh.material.shadowSide = THREE.BackSide;
map.csm.setupMaterial(mesh.material);
}
mesh.material.flatShading = self.properties.flatShading;
/* const previousOnAfterRender = mesh.onAfterRender;
mesh.onAfterRender = () => {
if(previousOnAfterRender) previousOnAfterRender();
if(mesh.geometry && mesh.geometry.attributes){
if (mesh.geometry.attributes.position) {
mesh.geometry.attributes.position.array = undefined;
if (mesh.geometry.attributes.position.data) {
mesh.geometry.attributes.position.data.array = undefined;
}
}
if (mesh.geometry.attributes.uv){
mesh.geometry.attributes.uv.array = undefined;
if (mesh.geometry.attributes.uv.data) {
mesh.geometry.attributes.uv.data.array = undefined;
}
}
if (mesh.geometry.attributes.normal) {
mesh.geometry.attributes.normal.array = undefined;
if (mesh.geometry.attributes.normal.data) {
mesh.geometry.attributes.normal.data.array = undefined;
}
}
}
if (mesh.material && mesh.material.map) {
mesh.material.map.mipmaps = undefined;
if (mesh.material.map.source) {
mesh.material.map.source.data = undefined;
}
}
mesh.onAfterRender = previousOnAfterRender;
} */
},
pointsCallback: (points, geometricError) => {
points.material.size = 1 * Math.max(1.0, 0.1 * Math.sqrt(geometricError));
points.material.sizeAttenuation = true;
points.material.receiveShadow = false;
points.material.castShadow = false;
}
});
this.tileset = new OGC3DTile({
url: this.url,
geometricErrorMultiplier: this.geometricErrorMultiplier,
loadOutsideView: this.loadOutsideView,
tileLoader: tileLoader,
renderer: map.renderer,
proxy: self.proxy,
static: true,
queryParams: self.queryParams,
displayErrors: self.displayErrors,
displayCopyright: self.displayCopyright,
centerModel: self.centerModel,
loadingStrategy: self.loadingStrategy
});
this.object3D = new THREE.Object3D();
this.object3D.matrixAutoUpdate = false;
this.object3D.add(this.tileset);
this.object3D.updateMatrix();
this.object3D.updateMatrixWorld(true);
}
_setPlanet(planet) {
this.planet = planet;
}
_addToScene(scene) {
this.scene = scene;
scene.add(this.object3D);
this.move(this._longitude, this._latitude, this._height, this._yaw, this._pitch, this._roll, this._scaleX, this._scaleY, this._scaleZ);
}
update(camera) {
if (!this.paused && this.visible) {
const stats = this.tileset.update(camera);
if (!!this.updateCallback) {
this.updateCallback(stats);
}
try{
this.tileset.tileLoader.update();
}catch(error){
//silence
}
}
}
/**
* Sets the object position and orientation based on Longitude, Latitude, Height, Yaw, Pitch, Roll
*
* @param {number} [longitude = 0] - a longitude in degrees
* @param {number} [latitude = 0] - a latitude in degrees
* @param {number} [height = 0] - a height in meters above WGS 84 sea level
* @param {number} [yaw = 0] - Yaw angle in degrees. (0 points north ccw rotation)
* @param {number} [pitch = 0] - Pitch angle in degrees (-90 to 90)
* @param {number} [roll = 0] - Roll angle in degrees.
* @param {number} [scaleX = 1] - scale on X axes.
* @param {number} [scaleY = 1] - scale on Y axes. defaults to the scaleX property if defined.
* @param {number} [scaleZ = 1] - scale on Z axes. defaults to the scaleX property if defined.
*/
move(longitude = 0, latitude = 0, height = 0, yaw = 0, pitch = 0, roll = 0, scaleX = 1, scaleY = 1, scaleZ = 1 ) {
this._longitude = longitude;
this._latitude = latitude;
this._height = height;
this._yaw = yaw;
this._pitch = pitch;
this._roll = roll;
this._scaleX = scaleX;
this._scaleY = scaleY;
this._scaleZ = scaleZ;
if(!this.planet) return;
rotation.set(
pitch*0.0174533, yaw*0.0174533, roll*0.0174533, "ZYX");
cartesianLocation.set(longitude, latitude, height);
llhToCartesianFastSFCT(cartesianLocation, false); // Convert LLH to Cartesian in-place
Up.copy(cartesianLocation).normalize();
East.crossVectors(Up, globalNorth).normalize();
if (East.lengthSq() === 0) {
East.set(1, 0, 0);
}
North.crossVectors(East, Up).normalize();
rotationMatrix.makeBasis(East, Up, North);
quaternionToEarthNormalOrientation.setFromRotationMatrix(rotationMatrix);
quaternionSelfRotation.setFromEuler(rotation);
this.object3D.quaternion.copy(quaternionToEarthNormalOrientation).multiply(quaternionSelfRotation);
this.object3D.position.copy(cartesianLocation);
this.object3D.scale.set(scaleX, scaleY, scaleZ);
this._updateMatrices();
}
_updateMatrices(){
this.object3D.updateMatrix();
this.object3D.updateMatrixWorld(true);
this.tileset.updateMatrices();
}
/* updateLocation() {
if (!this.planet) {
return;
}
if (this.llh) {
const transform = this.planet.llhToCartesian.forward(this.llh);
cartesianLocation.set(transform.x, transform.y, transform.z);
//quaternionSelfRotation
quaternionToEarthNormalOrientation.setFromUnitVectors(up, orientationHelper.copy(cartesianLocation).normalize());
quaternionSelfRotation.setFromEuler(this.rotation);
this.object3D.quaternion.copy(quaternionToEarthNormalOrientation).multiply(quaternionSelfRotation);
this.object3D.position.copy(cartesianLocation);
this.object3D.scale.set(this.scale, this.scale, this.scale);
}
this.object3D.updateMatrix();
this.object3D.updateMatrixWorld(true);
this.tileset.updateMatrices();
} */
dispose() {
this.scene.remove(this.object3D);
this.tileset.dispose();
}
getSelectableObjects() {
const selectable = [];
if (this.boundingMesh) selectable.push(this.boundingMesh);
return selectable;
}
select(objects) {
if (objects && objects.length && objects[0].layer == this && this.selectable) {
this.selected = true;
this.scene.add(this.selectionMesh);
this.scene.add(this.boundingMesh);
if (this.boundingMeshOutline) this.scene.add(this.boundingMeshOutline);
}
}
unselect(objects) {
if (objects && objects.length && objects[0].layer == this && this.selectable) {
this.selected = false;
this.scene.remove(this.selectionMesh);
this.scene.remove(this.boundingMesh);
if (this.boundingMeshOutline) this.scene.remove(this.boundingMeshOutline);
}
}
}
function _updateTileLoaders() {
tileLoaders.forEach(tileLoader => tileLoader.update());
}
export { OGC3DTilesLayer }