itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
342 lines (296 loc) • 14.4 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = exports.CAMERA_TYPE = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var THREE = _interopRequireWildcard(require("three"));
var _Coordinates = _interopRequireDefault(require("../Core/Geographic/Coordinates"));
var _DEMUtils = _interopRequireDefault(require("../Utils/DEMUtils"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/**
* @typedef {object} Camera~CAMERA_TYPE
* Stores the different types of camera usable in iTowns.
*
* @property {number} PERSPECTIVE Perspective type of camera
* @property {number} ORTHOGRAPHIC Orthographic type of camera
*/
var CAMERA_TYPE = {
PERSPECTIVE: 0,
ORTHOGRAPHIC: 1
};
exports.CAMERA_TYPE = CAMERA_TYPE;
var tmp = {
frustum: new THREE.Frustum(),
matrix: new THREE.Matrix4(),
box3: new THREE.Box3()
};
var ndcBox3 = new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1));
function updatePreSse(camera, height, fov) {
// sse = projected geometric error on screen plane from distance
// We're using an approximation, assuming that the geometric error of all
// objects is perpendicular to the camera view vector (= we always compute
// for worst case).
//
// screen plane object
// | __
// | / \
// | geometric{|
// < fov angle . } sse error {| |
// | \__/
// |
// |<--------------------->
// | distance
//
// geometric_error * screen_width (resp. screen_height)
// = ---------------------------------------
// 2 * distance * tan (horizontal_fov / 2) (resp. vertical_fov)
//
//
// We pre-compute the preSSE (= constant part of the screen space error formula) once here
// Note: the preSSE for the horizontal FOV is the same value
// focal = (this.height * 0.5) / Math.tan(verticalFOV * 0.5);
// horizontalFOV = 2 * Math.atan(this.width * 0.5 / focal);
// horizontalPreSSE = this.width / (2.0 * Math.tan(horizontalFOV * 0.5)); (1)
// => replacing horizontalFOV in Math.tan(horizontalFOV * 0.5)
// Math.tan(horizontalFOV * 0.5) = Math.tan(2 * Math.atan(this.width * 0.5 / focal) * 0.5)
// = Math.tan(Math.atan(this.width * 0.5 / focal))
// = this.width * 0.5 / focal
// => now replacing focal
// = this.width * 0.5 / (this.height * 0.5) / Math.tan(verticalFOV * 0.5)
// = Math.tan(verticalFOV * 0.5) * this.width / this.height
// => back to (1)
// horizontalPreSSE = this.width / (2.0 * Math.tan(verticalFOV * 0.5) * this.width / this.height)
// = this.height / 2.0 * Math.tan(verticalFOV * 0.5)
// = verticalPreSSE
if (camera.camera3D.isOrthographicCamera) {
camera._preSSE = height;
} else {
var verticalFOV = THREE.MathUtils.degToRad(fov);
camera._preSSE = height / (2.0 * Math.tan(verticalFOV * 0.5));
}
}
/**
* Wrapper around Three.js camera to expose some geographic helpers.
*
* @property {string} crs The camera's coordinate projection system.
* @property {object} camera3D The Three.js camera that is wrapped around.
* @property {number} width The width of the camera.
* @property {number} height The height of the camera.
* @property {number} _preSSE The precomputed constant part of the screen space error.
*/
var Camera = /*#__PURE__*/function () {
/**
* @param {string} crs The camera's coordinate projection system.
* @param {number} width The width (in pixels) of the view the
* camera is associated to.
* @param {number} height The height (in pixels) of the view the
* camera is associated to.
* @param {object} [options] Options for the camera.
* @param {THREE.Camera} [options.cameraThree] A custom Three.js camera object to wrap
* around.
* @param {Camera~CAMERA_TYPE} [options.type=CAMERA_TYPE.PERSPECTIVE] The type of the camera. See {@link
* CAMERA_TYPE}.
* @constructor
*/
function Camera(crs, width, height) {
var _this = this;
var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
(0, _classCallCheck2["default"])(this, Camera);
this.crs = crs;
if (options.isCamera) {
console.warn('options.camera parameter is deprecated. Use options.camera.cameraThree to place a custom ' + 'camera as a parameter. See the documentation of Camera.');
this.camera3D = options;
} else if (options.cameraThree) {
this.camera3D = options.cameraThree;
} else {
switch (options.type) {
case CAMERA_TYPE.ORTHOGRAPHIC:
this.camera3D = new THREE.OrthographicCamera();
break;
case CAMERA_TYPE.PERSPECTIVE:
default:
this.camera3D = new THREE.PerspectiveCamera(30);
break;
}
}
this.camera3D.aspect = this.camera3D.aspect !== undefined ? this.camera3D.aspect : 1;
this._viewMatrix = new THREE.Matrix4();
this.width = width;
this.height = height;
this._viewMatrixNeedsUpdate = true;
this.resize(width, height);
this._preSSE = Infinity;
if (this.camera3D.isPerspectiveCamera) {
var fov = this.camera3D.fov;
Object.defineProperty(this.camera3D, 'fov', {
get: function get() {
return fov;
},
set: function set(newFov) {
fov = newFov;
updatePreSse(_this, _this.height, fov);
}
});
}
}
/**
* Resize the camera to a given width and height
*
* @param {number} width The width to resize the camera to.
* @param {number} height The height to resize the camera to.
*/
(0, _createClass2["default"])(Camera, [{
key: "resize",
value: function resize(width, height) {
var ratio = width / height;
if (this.camera3D.aspect !== ratio) {
if (this.camera3D.isOrthographicCamera) {
this.camera3D.zoom *= this.width / width;
var halfH = this.camera3D.top * this.camera3D.aspect / ratio;
this.camera3D.bottom = -halfH;
this.camera3D.top = halfH;
} else if (this.camera3D.isPerspectiveCamera) {
this.camera3D.fov = 2 * THREE.Math.radToDeg(Math.atan(height / this.height * Math.tan(THREE.Math.degToRad(this.camera3D.fov) / 2)));
}
this.camera3D.aspect = ratio;
}
this.width = width;
this.height = height;
updatePreSse(this, this.height, this.camera3D.fov);
if (this.camera3D.updateProjectionMatrix) {
this.camera3D.updateProjectionMatrix();
this._viewMatrixNeedsUpdate = true;
}
}
}, {
key: "update",
value: function update() {
// update matrix
this.camera3D.updateMatrixWorld();
this._viewMatrixNeedsUpdate = true;
}
/**
* Return the position in the requested CRS, or in camera's CRS if undefined.
*
* @param {string} [crs] If defined (e.g 'EPSG:4326'), the camera position will be returned in this CRS.
*
* @return {Coordinates} Coordinates object holding camera's position.
*/
}, {
key: "position",
value: function position(crs) {
return new _Coordinates["default"](this.crs, this.camera3D.position).as(crs || this.crs);
}
/**
* Set the position of the camera using a Coordinates object.
* If you want to modify the position directly using x,y,z values then use `camera.camera3D.position.set(x, y, z)`
*
* @param {Coordinates} position The new position of the camera.
*/
}, {
key: "setPosition",
value: function setPosition(position) {
this.camera3D.position.copy(position.as(this.crs));
}
}, {
key: "isBox3Visible",
value: function isBox3Visible(box3, matrixWorld) {
return this.box3SizeOnScreen(box3, matrixWorld).intersectsBox(ndcBox3);
}
}, {
key: "isSphereVisible",
value: function isSphereVisible(sphere, matrixWorld) {
if (this._viewMatrixNeedsUpdate) {
// update visibility testing matrix
this._viewMatrix.multiplyMatrices(this.camera3D.projectionMatrix, this.camera3D.matrixWorldInverse);
this._viewMatrixNeedsUpdate = false;
}
if (matrixWorld) {
tmp.matrix.multiplyMatrices(this._viewMatrix, matrixWorld);
tmp.frustum.setFromProjectionMatrix(tmp.matrix);
} else {
tmp.frustum.setFromProjectionMatrix(this._viewMatrix);
}
return tmp.frustum.intersectsSphere(sphere);
}
}, {
key: "box3SizeOnScreen",
value: function box3SizeOnScreen(box3, matrixWorld) {
var pts = projectBox3PointsInCameraSpace(this, box3, matrixWorld); // All points are in front of the near plane -> box3 is invisible
if (!pts) {
return tmp.box3.makeEmpty();
} // Project points on screen
for (var i = 0; i < 8; i++) {
pts[i].applyMatrix4(this.camera3D.projectionMatrix);
}
return tmp.box3.setFromPoints(pts);
}
/**
* Test for collision between camera and a geometry layer (DTM/DSM) to adjust camera position.
* It could be modified later to handle an array of geometry layers.
* TODO Improve Coordinates class to handle altitude for any coordinate system (even projected one)
*
* @param {View} view The view where we test the collision between geometry layers
* and the camera
* @param {ElevationLayer} elevationLayer The elevation layer (DTM/DSM) used to test the collision
* with the camera. Could be another geometry layer.
* @param {number} minDistanceCollision The minimum distance allowed between the camera and the
* surface.
*/
}, {
key: "adjustAltitudeToAvoidCollisionWithLayer",
value: function adjustAltitudeToAvoidCollisionWithLayer(view, elevationLayer, minDistanceCollision) {
// We put the camera location in geographic by default to easily handle altitude.
// (Should be improved in Coordinates class for all ref)
var camLocation = view.camera.position().as('EPSG:4326');
if (elevationLayer !== undefined) {
var elevationUnderCamera = _DEMUtils["default"].getElevationValueAt(elevationLayer, camLocation);
if (elevationUnderCamera !== undefined) {
var difElevation = camLocation.altitude - (elevationUnderCamera + minDistanceCollision); // We move the camera to avoid collision if too close to terrain
if (difElevation < 0) {
camLocation.altitude = elevationUnderCamera + minDistanceCollision;
view.camera.camera3D.position.copy(camLocation.as(view.referenceCrs));
view.notifyChange(this.camera3D);
}
}
}
}
}]);
return Camera;
}();
var points = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
function projectBox3PointsInCameraSpace(camera, box3, matrixWorld) {
// Projects points in camera space
// We don't project directly on screen to avoid artifacts when projecting
// points behind the near plane.
var m = camera.camera3D.matrixWorldInverse;
if (matrixWorld) {
m = tmp.matrix.multiplyMatrices(camera.camera3D.matrixWorldInverse, matrixWorld);
}
points[0].set(box3.min.x, box3.min.y, box3.min.z).applyMatrix4(m);
points[1].set(box3.min.x, box3.min.y, box3.max.z).applyMatrix4(m);
points[2].set(box3.min.x, box3.max.y, box3.min.z).applyMatrix4(m);
points[3].set(box3.min.x, box3.max.y, box3.max.z).applyMatrix4(m);
points[4].set(box3.max.x, box3.min.y, box3.min.z).applyMatrix4(m);
points[5].set(box3.max.x, box3.min.y, box3.max.z).applyMatrix4(m);
points[6].set(box3.max.x, box3.max.y, box3.min.z).applyMatrix4(m);
points[7].set(box3.max.x, box3.max.y, box3.max.z).applyMatrix4(m); // In camera space objects are along the -Z axis
// So if min.z is > -near, the object is invisible
var atLeastOneInFrontOfNearPlane = false;
for (var i = 0; i < 8; i++) {
if (points[i].z <= -camera.camera3D.near) {
atLeastOneInFrontOfNearPlane = true;
} else {
// Clamp to near plane
points[i].z = -camera.camera3D.near;
}
}
return atLeastOneInFrontOfNearPlane ? points : undefined;
}
var _default = Camera;
exports["default"] = _default;