/** * @module olcs.contrib.Manager */ import olcsContribLazyLoader from '../contrib/LazyLoader.js'; import OLCesium from '../OLCesium.js'; import olcsCore from '../core.js'; import {toRadians} from '../math.js'; import olObservable from 'ol/Observable.js'; /** * @typedef {Object} ManagerOptions * @property {import('ol/Map.js').default} map * @property {import('ol/extent.js').Extent} [cameraExtentInRadians] * @property {string} [cesiumIonDefaultAccessToken] */ const Manager = class extends olObservable { /** * @param {string} cesiumUrl * @param {olcsx.contrib.ManagerOptions} options * @api */ constructor(cesiumUrl, {map, cameraExtentInRadians, cesiumIonDefaultAccessToken} = {}) { super(); /** * @type {string} * @private */ this.cesiumUrl_ = cesiumUrl; /** * @type {ol.Map} * @protected */ this.map = map; /** * @type {ol.Extent} * @protected */ this.cameraExtentInRadians = cameraExtentInRadians || null; /** * @private * @type {Cesium.BoundingSphere} */ this.boundingSphere_; /** * @type {boolean} * @private */ this.blockLimiter_ = false; /** * @type {Promise.} * @private */ this.promise_; /** * @type {string} * @private */ this.cesiumIonDefaultAccessToken_ = cesiumIonDefaultAccessToken; /** * @type {olcs.OLCesium} * @protected */ this.ol3d; /** * @const {number} Tilt angle in radians * @private */ this.cesiumInitialTilt_ = toRadians(50); /** * @protected * @type {number} */ this.fogDensity = 0.0001; /** * @protected * @type {number} */ this.fogSSEFactor = 25; /** * Limit the minimum distance to the terrain to 2m. * @protected * @type {number} */ this.minimumZoomDistance = 2; /** * Limit the maximum distance to the earth to 10'000km. * @protected * @type {number} */ this.maximumZoomDistance = 10000000; // when closer to 3000m, restrict the available positions harder /** * @protected * @param {number} height */ this.limitCameraToBoundingSphereRatio = height => (height > 3000 ? 9 : 3); } /** * @return {Promise.} */ load() { if (!this.promise_) { const cesiumLazyLoader = new olcsContribLazyLoader(this.cesiumUrl_); this.promise_ = cesiumLazyLoader.load().then(() => this.onCesiumLoaded()); } return this.promise_; } /** * @protected * @return {olcs.OLCesium} */ onCesiumLoaded() { if (this.cameraExtentInRadians) { const rect = new Cesium.Rectangle(...this.cameraExtentInRadians); // Set the fly home rectangle Cesium.Camera.DEFAULT_VIEW_RECTANGLE = rect; this.boundingSphere_ = Cesium.BoundingSphere.fromRectangle3D(rect, Cesium.Ellipsoid.WGS84, 300); // lux mean height is 300m } if (this.cesiumIonDefaultAccessToken_) { Cesium.Ion.defaultAccessToken = this.cesiumIonDefaultAccessToken_; } this.ol3d = this.instantiateOLCesium(); const scene = this.ol3d.getCesiumScene(); this.configureForUsability(scene); this.configureForPerformance(scene); this.dispatchEvent('load'); return this.ol3d; } /** * Application code should override this method. * @return {olcs.OLCesium} */ instantiateOLCesium() { console.assert(this.map); const ol3d = new OLCesium({map: this.map}); const scene = ol3d.getCesiumScene(); const terrainProvider = Cesium.createWorldTerrain(); scene.terrainProvider = terrainProvider; return ol3d; } /** * @param {!Cesium.Scene} scene The scene, passed as parameter for convenience. * @protected */ configureForPerformance(scene) { const fog = scene.fog; fog.enabled = true; fog.density = this.fogDensity; fog.screenSpaceErrorFactor = this.fogSSEFactor; } /** * @param {!Cesium.Scene} scene The scene, passed as parameter for convenience. * @protected */ configureForUsability(scene) { const sscController = scene.screenSpaceCameraController; sscController.minimumZoomDistance = this.minimumZoomDistance; sscController.maximumZoomDistance = this.maximumZoomDistance; // Do not see through the terrain. Seeing through the terrain does not make // sense anyway, except for debugging scene.globe.depthTestAgainstTerrain = true; // Use white instead of the black default colour for the globe when tiles are missing scene.globe.baseColor = Cesium.Color.WHITE; scene.backgroundColor = Cesium.Color.WHITE; if (this.boundingSphere_) { scene.postRender.addEventListener(this.limitCameraToBoundingSphere.bind(this), scene); } // Stop rendering Cesium when there is nothing to do. This drastically reduces CPU/GPU consumption. this.ol3d.enableAutoRenderLoop(); } /** * Constrain the camera so that it stays close to the bounding sphere of the map extent. * Near the ground the allowed distance is shorter. * @protected */ limitCameraToBoundingSphere() { if (this.boundingSphere_ && !this.blockLimiter_) { const scene = this.ol3d.getCesiumScene(); const camera = scene.camera; const position = camera.position; const carto = Cesium.Cartographic.fromCartesian(position); const ratio = this.limitCameraToBoundingSphereRatio(carto.height); if (Cesium.Cartesian3.distance(this.boundingSphere_.center, position) > this.boundingSphere_.radius * ratio) { const currentlyFlying = camera.flying; if (currentlyFlying === true) { // There is a flying property and its value is true return; } else { this.blockLimiter_ = true; const unblockLimiter = () => this.blockLimiter_ = false; camera.flyToBoundingSphere(this.boundingSphere_, { complete: unblockLimiter, cancel: unblockLimiter }); } } } } /** * Enable or disable ol3d with a default animation. * @export * @return {Promise} */ toggle3d() { return this.load().then((/** @const {!olcs.OLCesium} */ ol3d) => { const is3DCurrentlyEnabled = ol3d.getEnabled(); const scene = ol3d.getCesiumScene(); if (is3DCurrentlyEnabled) { // Disable 3D console.assert(this.map); return olcsCore.resetToNorthZenith(this.map, scene).then(() => { ol3d.setEnabled(false); this.dispatchEvent('toggle'); }); } else { // Enable 3D ol3d.setEnabled(true); this.dispatchEvent('toggle'); return olcsCore.rotateAroundBottomCenter(scene, this.cesiumInitialTilt_); } }); } /** * Enable ol3d with a view built from parameters. * * @export * @param {number} lon * @param {number} lat * @param {number} elevation * @param {number} headingDeg Heading value in degrees. * @param {number} pitchDeg Pitch value in degrees. * @returns {Promise} */ set3dWithView(lon, lat, elevation, headingDeg, pitchDeg) { return this.load().then((/** @const {!olcs.OLCesium} */ ol3d) => { const is3DCurrentlyEnabled = ol3d.getEnabled(); const scene = ol3d.getCesiumScene(); const camera = scene.camera; const destination = Cesium.Cartesian3.fromDegrees(lon, lat, elevation); const heading = Cesium.Math.toRadians(headingDeg); const pitch = Cesium.Math.toRadians(pitchDeg); const roll = 0; const orientation = {heading, pitch, roll}; if (!is3DCurrentlyEnabled) { ol3d.setEnabled(true); this.dispatchEvent('toggle'); } camera.setView({ destination, orientation }); }); } /** * @export * @return {boolean} */ is3dEnabled() { return !!this.ol3d && this.ol3d.getEnabled(); } /** * @return {number} */ getHeading() { return this.map ? this.map.getView().getRotation() || 0 : 0; } /** * @return {number|undefined} */ getTiltOnGlobe() { const scene = this.ol3d.getCesiumScene(); const tiltOnGlobe = olcsCore.computeSignedTiltAngleOnGlobe(scene); return -tiltOnGlobe; } /** * @param {number} angle */ setHeading(angle) { const scene = this.ol3d.getCesiumScene(); const bottom = olcsCore.pickBottomPoint(scene); if (bottom) { olcsCore.setHeadingUsingBottomCenter(scene, angle, bottom); } } /** * @export * @return {olcs.OLCesium} */ getOl3d() { return this.ol3d; } /** * @export * @return {!ol.View} */ getOlView() { const view = this.map.getView(); console.assert(view); return view; } /** * @export * @return {Cesium.Matrix4} */ getCesiumViewMatrix() { return this.ol3d.getCesiumScene().camera.viewMatrix; } /** * @export * @return {!Cesium.Scene} */ getCesiumScene() { return this.ol3d.getCesiumScene(); } /** * @export * @param {!Cesium.Rectangle} rectangle * @param {number=} offset in meters * @return {Promise} */ flyToRectangle(rectangle, offset = 0) { const camera = this.getCesiumScene().camera; const destination = camera.getRectangleCameraCoordinates(rectangle); const mag = Cesium.Cartesian3.magnitude(destination) + offset; Cesium.Cartesian3.normalize(destination, destination); Cesium.Cartesian3.multiplyByScalar(destination, mag, destination); return new Promise((resolve, reject) => { if (!this.cameraExtentInRadians) { reject(); return; } camera.flyTo({ destination, complete: () => resolve(), cancel: () => reject(), endTransform: Cesium.Matrix4.IDENTITY }); }); } /** * @protected * @return {Cesium.Rectangle|undefined} */ getCameraExtentRectangle() { if (this.cameraExtentInRadians) { return new Cesium.Rectangle(...this.cameraExtentInRadians); } } }; export default Manager;