UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

342 lines (296 loc) 14.4 kB
"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;