UNPKG

terriajs

Version:

Geospatial data visualization platform.

1,009 lines (829 loc) 41.5 kB
'use strict'; /*global require*/ var Cartesian2 = require('terriajs-cesium/Source/Core/Cartesian2'); var Cartesian3 = require('terriajs-cesium/Source/Core/Cartesian3'); var Cartographic = require('terriajs-cesium/Source/Core/Cartographic'); var CesiumMath = require('terriajs-cesium/Source/Core/Math'); var defaultValue = require('terriajs-cesium/Source/Core/defaultValue'); var defined = require('terriajs-cesium/Source/Core/defined'); var destroyObject = require('terriajs-cesium/Source/Core/destroyObject'); var DeveloperError = require('terriajs-cesium/Source/Core/DeveloperError'); var Ellipsoid = require('terriajs-cesium/Source/Core/Ellipsoid'); var Entity = require('terriajs-cesium/Source/DataSources/Entity'); var formatError = require('terriajs-cesium/Source/Core/formatError'); var getTimestamp = require('terriajs-cesium/Source/Core/getTimestamp'); var ImageryLayer = require('terriajs-cesium/Source/Scene/ImageryLayer'); var JulianDate = require('terriajs-cesium/Source/Core/JulianDate'); var knockout = require('terriajs-cesium/Source/ThirdParty/knockout'); var loadWithXhr = require('../Core/loadWithXhr'); var Matrix4 = require('terriajs-cesium/Source/Core/Matrix4'); var Rectangle = require('terriajs-cesium/Source/Core/Rectangle'); var sampleTerrain = require('terriajs-cesium/Source/Core/sampleTerrain'); var SceneTransforms = require('terriajs-cesium/Source/Scene/SceneTransforms'); var ScreenSpaceEventType = require('terriajs-cesium/Source/Core/ScreenSpaceEventType'); var TaskProcessor = require('terriajs-cesium/Source/Core/TaskProcessor'); var Transforms = require('terriajs-cesium/Source/Core/Transforms'); var when = require('terriajs-cesium/Source/ThirdParty/when'); var EventHelper = require('terriajs-cesium/Source/Core/EventHelper'); var ImagerySplitDirection = require('terriajs-cesium/Source/Scene/ImagerySplitDirection'); var CesiumSelectionIndicator = require('../Map/CesiumSelectionIndicator'); var Feature = require('./Feature'); var GlobeOrMap = require('./GlobeOrMap'); var inherit = require('../Core/inherit'); var TerriaError = require('../Core/TerriaError'); var PickedFeatures = require('../Map/PickedFeatures'); var ViewerMode = require('./ViewerMode'); /** * The Cesium viewer component * * @alias Cesium * @constructor * @extends GlobeOrMap * * @param {Terria} terria The Terria instance. * @param {Viewer} viewer The Cesium viewer instance. */ var Cesium = function(terria, viewer) { GlobeOrMap.call(this, terria); /** * Gets or sets the Cesium {@link Viewer} instance. * @type {Viewer} */ this.viewer = viewer; /** * Gets or sets the Cesium {@link Scene} instance. * @type {Scene} */ this.scene = viewer.scene; /** * Gets or sets whether the viewer has stopped rendering since startup or last set to false. * @type {Boolean} */ this.stoppedRendering = false; /** * Gets or sets whether to output info to the console when starting and stopping rendering loop. * @type {Boolean} */ this.verboseRendering = false; /** * Gets or sets whether this viewer _can_ show a splitter. * @type {Boolean} */ this.canShowSplitter = true; this._lastClockTime = new JulianDate(0, 0.0); this._lastCameraViewMatrix = new Matrix4(); this._lastCameraMoveTime = 0; this._selectionIndicator = new CesiumSelectionIndicator(this); this._removePostRenderListener = this.scene.postRender.addEventListener(postRender.bind(undefined, this)); this._removeInfoBoxCloseListener = undefined; this._boundNotifyRepaintRequired = this.notifyRepaintRequired.bind(this); this._pauseMapInteractionCount = 0; this.scene.imagerySplitPosition = this.terria.splitPosition; // Handle left click by picking objects from the map. viewer.screenSpaceEventHandler.setInputAction(function(e) { this.pickFromScreenPosition(e.position); }.bind(this), ScreenSpaceEventType.LEFT_CLICK); // Force a repaint when the mouse moves or the window changes size. var canvas = this.viewer.canvas; canvas.addEventListener('mousemove', this._boundNotifyRepaintRequired, false); canvas.addEventListener('mousedown', this._boundNotifyRepaintRequired, false); canvas.addEventListener('mouseup', this._boundNotifyRepaintRequired, false); canvas.addEventListener('touchstart', this._boundNotifyRepaintRequired, false); canvas.addEventListener('touchend', this._boundNotifyRepaintRequired, false); canvas.addEventListener('touchmove', this._boundNotifyRepaintRequired, false); if (defined(window.PointerEvent)) { canvas.addEventListener('pointerdown', this._boundNotifyRepaintRequired, false); canvas.addEventListener('pointerup', this._boundNotifyRepaintRequired, false); canvas.addEventListener('pointermove', this._boundNotifyRepaintRequired, false); } // Detect available wheel event this._wheelEvent = undefined; if ('onwheel' in canvas) { // spec event type this._wheelEvent = 'wheel'; } else if (defined(document.onmousewheel)) { // legacy event type this._wheelEvent = 'mousewheel'; } else { // older Firefox this._wheelEvent = 'DOMMouseScroll'; } canvas.addEventListener(this._wheelEvent, this._boundNotifyRepaintRequired, false); window.addEventListener('resize', this._boundNotifyRepaintRequired, false); // Force a repaint when the feature info box is closed. Cesium can't close its info box // when the clock is not ticking, for reasons that are not clear. if (defined(this.viewer.infoBox)) { this._removeInfoBoxCloseListener = this.viewer.infoBox.viewModel.closeClicked.addEventListener(this._boundNotifyRepaintRequired); } if (defined(this.viewer._clockViewModel)) { var clock = this.viewer._clockViewModel; this._shouldAnimateSubscription = knockout.getObservable(clock, 'shouldAnimate').subscribe(this._boundNotifyRepaintRequired); this._currentTimeSubscription = knockout.getObservable(clock, 'currentTime').subscribe(this._boundNotifyRepaintRequired); } if (defined(this.viewer.timeline)) { this.viewer.timeline.addEventListener('settime', this._boundNotifyRepaintRequired, false); } this._selectedFeatureSubscription = knockout.getObservable(this.terria, 'selectedFeature').subscribe(function() { selectFeature(this); }, this); this._splitterPositionSubscription = knockout.getObservable(this.terria, 'splitPosition').subscribe(function() { if (this.scene) { this.scene.imagerySplitPosition = this.terria.splitPosition; this.notifyRepaintRequired(); } }, this); this._showSplitterSubscription = knockout.getObservable(terria, 'showSplitter').subscribe(function() { this.updateAllItemsForSplitter(); }, this); // Hacky way to force a repaint when an async load request completes var that = this; this._originalLoadWithXhr = loadWithXhr.load; loadWithXhr.load = function(url, responseType, method, data, headers, deferred, overrideMimeType, preferText, timeout) { deferred.promise.always(that._boundNotifyRepaintRequired); that._originalLoadWithXhr(url, responseType, method, data, headers, deferred, overrideMimeType, preferText, timeout); }; // Hacky way to force a repaint when a web worker sends something back. this._originalScheduleTask = TaskProcessor.prototype.scheduleTask; TaskProcessor.prototype.scheduleTask = function(parameters, transferableObjects) { var result = that._originalScheduleTask.call(this, parameters, transferableObjects); if (!defined(this._originalWorkerMessageSinkRepaint)) { this._originalWorkerMessageSinkRepaint = this._worker.onmessage; var taskProcessor = this; this._worker.onmessage = function(event) { taskProcessor._originalWorkerMessageSinkRepaint(event); if (that.isDestroyed()) { taskProcessor._worker.onmessage = taskProcessor._originalWorkerMessageSinkRepaint; taskProcessor._originalWorkerMessageSinkRepaint = undefined; } else { that.notifyRepaintRequired(); } }; } return result; }; this.eventHelper = new EventHelper(); // If the render loop crashes, inform the user and then switch to 2D. this.eventHelper.add(this.scene.renderError, function(scene, error) { this.terria.error.raiseEvent(new TerriaError({ sender: this, title: 'Error rendering in 3D', message: '\ <p>An error occurred while rendering in 3D. This probably indicates a bug in ' + terria.appName + ' or an incompatibility with your system \ or web browser. We\'ll now switch you to 2D so that you can continue your work. We would appreciate it if you report this \ error by sending an email to <a href="mailto:' + terria.supportEmail + '">' + terria.supportEmail + '</a> with the \ technical details below. Thank you!</p><pre>' + formatError(error) + '</pre>' })); this.terria.viewerMode = ViewerMode.Leaflet; }, this); this.eventHelper.add(this.scene.globe.tileLoadProgressEvent, this.updateTilesLoadingCount.bind(this)); selectFeature(this); }; inherit(GlobeOrMap, Cesium); Cesium.prototype.destroy = function() { if (defined(this._selectionIndicator)) { this._selectionIndicator.destroy(); this._selectionIndicator = undefined; } if (defined(this._removePostRenderListener)) { this._removePostRenderListener(); this._removePostRenderListener = undefined; } if (defined(this._removeInfoBoxCloseListener)) { this._removeInfoBoxCloseListener(); } if (defined(this._shouldAnimateSubscription)) { this._shouldAnimateSubscription.dispose(); this._shouldAnimateSubscription = undefined; } if (defined(this._currentTimeSubscription)) { this._currentTimeSubscription.dispose(); this._currentTimeSubscription = undefined; } if (defined(this.viewer.timeline)) { this.viewer.timeline.removeEventListener('settime', this._boundNotifyRepaintRequired, false); } if (defined(this._selectedFeatureSubscription)) { this._selectedFeatureSubscription.dispose(); this._selectedFeatureSubscription = undefined; } if (defined(this._splitterPositionSubscription)) { this._splitterPositionSubscription.dispose(); this._splitterPositionSubscription = undefined; } if (defined(this._showSplitterSubscription)) { this._showSplitterSubscription.dispose(); this._showSplitterSubscription = undefined; } this.viewer.canvas.removeEventListener('mousemove', this._boundNotifyRepaintRequired, false); this.viewer.canvas.removeEventListener('mousedown', this._boundNotifyRepaintRequired, false); this.viewer.canvas.removeEventListener('mouseup', this._boundNotifyRepaintRequired, false); this.viewer.canvas.removeEventListener('touchstart', this._boundNotifyRepaintRequired, false); this.viewer.canvas.removeEventListener('touchend', this._boundNotifyRepaintRequired, false); this.viewer.canvas.removeEventListener('touchmove', this._boundNotifyRepaintRequired, false); if (defined(window.PointerEvent)) { this.viewer.canvas.removeEventListener('pointerdown', this._boundNotifyRepaintRequired, false); this.viewer.canvas.removeEventListener('pointerup', this._boundNotifyRepaintRequired, false); this.viewer.canvas.removeEventListener('pointermove', this._boundNotifyRepaintRequired, false); } this.viewer.canvas.removeEventListener(this._wheelEvent, this._boundNotifyRepaintRequired, false); window.removeEventListener('resize', this._boundNotifyRepaintRequired, false); loadWithXhr.load = this._originalLoadWithXhr; TaskProcessor.prototype.scheduleTask = this._originalScheduleTask; this.eventHelper.removeAll(); GlobeOrMap.disposeCommonListeners(this); return destroyObject(this); }; Cesium.prototype.isDestroyed = function() { return false; }; var cartesian3Scratch = new Cartesian3(); var enuToFixedScratch = new Matrix4(); var southwestScratch = new Cartesian3(); var southeastScratch = new Cartesian3(); var northeastScratch = new Cartesian3(); var northwestScratch = new Cartesian3(); var southwestCartographicScratch = new Cartographic(); var southeastCartographicScratch = new Cartographic(); var northeastCartographicScratch = new Cartographic(); var northwestCartographicScratch = new Cartographic(); /** * Gets the current extent of the camera. This may be approximate if the viewer does not have a strictly rectangular view. * @return {Rectangle} The current visible extent. */ Cesium.prototype.getCurrentExtent = function() { var scene = this.scene; var camera = scene.camera; var width = scene.canvas.clientWidth; var height = scene.canvas.clientHeight; var centerOfScreen = new Cartesian2(width / 2.0, height / 2.0); var pickRay = scene.camera.getPickRay(centerOfScreen); var center = scene.globe.pick(pickRay, scene); if (!defined(center)) { // TODO: binary search to find the horizon point and use that as the center. return this.terria.homeView.rectangle; } var ellipsoid = this.scene.globe.ellipsoid; var fovy = scene.camera.frustum.fovy * 0.5; var fovx = Math.atan(Math.tan(fovy) * scene.camera.frustum.aspectRatio); var cameraOffset = Cartesian3.subtract(camera.positionWC, center, cartesian3Scratch); var cameraHeight = Cartesian3.magnitude(cameraOffset); var xDistance = cameraHeight * Math.tan(fovx); var yDistance = cameraHeight * Math.tan(fovy); var southwestEnu = new Cartesian3(-xDistance, -yDistance, 0.0); var southeastEnu = new Cartesian3(xDistance, -yDistance, 0.0); var northeastEnu = new Cartesian3(xDistance, yDistance, 0.0); var northwestEnu = new Cartesian3(-xDistance, yDistance, 0.0); var enuToFixed = Transforms.eastNorthUpToFixedFrame(center, ellipsoid, enuToFixedScratch); var southwest = Matrix4.multiplyByPoint(enuToFixed, southwestEnu, southwestScratch); var southeast = Matrix4.multiplyByPoint(enuToFixed, southeastEnu, southeastScratch); var northeast = Matrix4.multiplyByPoint(enuToFixed, northeastEnu, northeastScratch); var northwest = Matrix4.multiplyByPoint(enuToFixed, northwestEnu, northwestScratch); var southwestCartographic = ellipsoid.cartesianToCartographic(southwest, southwestCartographicScratch); var southeastCartographic = ellipsoid.cartesianToCartographic(southeast, southeastCartographicScratch); var northeastCartographic = ellipsoid.cartesianToCartographic(northeast, northeastCartographicScratch); var northwestCartographic = ellipsoid.cartesianToCartographic(northwest, northwestCartographicScratch); // Account for date-line wrapping if (southeastCartographic.longitude < southwestCartographic.longitude) { southeastCartographic.longitude += CesiumMath.TWO_PI; } if (northeastCartographic.longitude < northwestCartographic.longitude) { northeastCartographic.longitude += CesiumMath.TWO_PI; } var rect = new Rectangle( CesiumMath.convertLongitudeRange(Math.min(southwestCartographic.longitude, northwestCartographic.longitude)), Math.min(southwestCartographic.latitude, southeastCartographic.latitude), CesiumMath.convertLongitudeRange(Math.max(northeastCartographic.longitude, southeastCartographic.longitude)), Math.max(northeastCartographic.latitude, northwestCartographic.latitude)); rect.center = center; return rect; }; /** * Gets the current container element. * @return {Element} The current container element. */ Cesium.prototype.getContainer = function() { return this.viewer.container; }; /** * Zooms to a specified camera view or extent with a smooth flight animation. * * @param {CameraView|Rectangle} viewOrExtent The view or extent to which to zoom. * @param {Number} [flightDurationSeconds=3.0] The length of the flight animation in seconds. */ Cesium.prototype.zoomTo = function(viewOrExtent, flightDurationSeconds) { if (!defined(viewOrExtent)) { throw new DeveloperError('viewOrExtent is required.'); } flightDurationSeconds = defaultValue(flightDurationSeconds, 3.0); var that = this; return when().then(function() { if (viewOrExtent instanceof Rectangle) { var camera = that.scene.camera; // Work out the destination that the camera would naturally fly to var destinationCartesian = camera.getRectangleCameraCoordinates(viewOrExtent); var destination = Ellipsoid.WGS84.cartesianToCartographic(destinationCartesian); var terrainProvider = that.scene.globe.terrainProvider; var level = 6; // A sufficiently coarse tile level that still has approximately accurate height var positions = [Rectangle.center(viewOrExtent)]; // Perform an elevation query at the centre of the rectangle return sampleTerrain(terrainProvider, level, positions).then(function(results) { // Add terrain elevation to camera altitude var finalDestinationCartographic = { longitude: destination.longitude, latitude: destination.latitude, height: destination.height + results[0].height }; var finalDestination = Ellipsoid.WGS84.cartographicToCartesian(finalDestinationCartographic); camera.flyTo({ duration: flightDurationSeconds, destination: finalDestination }); }); } else if (defined(viewOrExtent.position)) { that.scene.camera.flyTo({ duration: flightDurationSeconds, destination: viewOrExtent.position, orientation: { direction: viewOrExtent.direction, up: viewOrExtent.up } }); } else { that.scene.camera.flyTo({ duration: flightDurationSeconds, destination: viewOrExtent.rectangle }); } }).then(function() { that.notifyRepaintRequired(); }); }; /** * Captures a screenshot of the map. * @return {Promise} A promise that resolves to a data URL when the screenshot is ready. */ Cesium.prototype.captureScreenshot = function() { var deferred = when.defer(); var removeCallback = this.scene.postRender.addEventListener(function() { removeCallback(); try { const cesiumCanvas = this.scene.canvas; // If we're using the splitter, draw the split position as a vertical white line. let canvas = cesiumCanvas; if (this.terria.showSplitter) { canvas = document.createElement('canvas'); canvas.width = cesiumCanvas.width; canvas.height = cesiumCanvas.height; const context = canvas.getContext('2d'); context.drawImage(cesiumCanvas, 0, 0); const x = this.terria.splitPosition * cesiumCanvas.width; context.strokeStyle = this.terria.baseMapContrastColor; context.beginPath(); context.moveTo(x, 0); context.lineTo(x, cesiumCanvas.height); context.stroke(); } deferred.resolve(canvas.toDataURL('image/png')); } catch (e) { deferred.reject(e); } }, this); this.scene.render(this.terria.clock.currentTime); return deferred.promise; }; /** * Notifies the viewer that a repaint is required. */ Cesium.prototype.notifyRepaintRequired = function() { if (this.verboseRendering && !this.viewer.useDefaultRenderLoop) { console.log('starting rendering @ ' + getTimestamp()); } this._lastCameraMoveTime = getTimestamp(); this.viewer.useDefaultRenderLoop = true; }; /** * Computes the screen position of a given world position. * @param {Cartesian3} position The world position in Earth-centered Fixed coordinates. * @param {Cartesian2} [result] The instance to which to copy the result. * @return {Cartesian2} The screen position, or undefined if the position is not on the screen. */ Cesium.prototype.computePositionOnScreen = function(position, result) { return SceneTransforms.wgs84ToWindowCoordinates(this.scene, position, result); }; /** * Adds an attribution to the globe. * @param {Credit} attribution The attribution to add. */ Cesium.prototype.addAttribution = function(attribution) { if (attribution) { this.scene.frameState.creditDisplay.addDefaultCredit(attribution); } }; /** * Removes an attribution from the globe. * @param {Credit} attribution The attribution to remove. */ Cesium.prototype.removeAttribution = function(attribution) { if (attribution) { this.scene.frameState.creditDisplay.removeDefaultCredit(attribution); } }; /** * Gets all attribution currently active on the globe or map. * @returns {String[]} The list of current attributions, as HTML strings. */ Cesium.prototype.getAllAttribution = function() { const credits = this.scene.frameState.creditDisplay._currentFrameCredits.screenCredits.values.concat(this.scene.frameState.creditDisplay._currentFrameCredits.lightboxCredits.values); return credits.map(credit => credit.html); }; /** * Updates the order of layers, moving layers where {@link CatalogItem#keepOnTop} is true to the top. */ Cesium.prototype.updateLayerOrderToKeepOnTop = function() { // move alwaysOnTop layers to the top var items = this.terria.nowViewing.items; var scene = this.scene; for (var l = items.length - 1; l >= 0; l--) { if (items[l].imageryLayer && items[l].keepOnTop) { scene.imageryLayers.raiseToTop(items[l].imageryLayer); } } }; Cesium.prototype.updateLayerOrderAfterReorder = function() { // because this Cesium model does the reordering via raise and lower, no action needed. }; // useful for counting the number of items in composite and non-composite items function countNumberOfSubItems(item) { if (defined(item.items)) { return item.items.length; } else { return 1; } } /** * Raise an item's level in the viewer * This does not check that index is valid * @param {Number} index The index of the item to raise */ Cesium.prototype.raise = function(index) { var items = this.terria.nowViewing.items; var item = items[index]; var itemAbove = items[index - 1]; if (!defined(itemAbove.items) && !defined(itemAbove.imageryLayer)) { return; } // Both item and itemAbove may either have a single imageryLayer, or be a composite item // Composite items have an items array of further items. // Define n as the number of subitems in ItemAbove (1 except for composites) // if item is a composite, then raise each subitem in item n times, // starting with the one at the top - which is the last one in the list // if item is not a composite, just raise the item n times directly. var n = countNumberOfSubItems(itemAbove); var i, j, subItem; if (defined(item.items)) { for (i = item.items.length - 1; i >= 0; --i) { subItem = item.items[i]; if (defined(subItem.imageryLayer)) { for (j = 0; j < n; ++j) { this.scene.imageryLayers.raise(subItem.imageryLayer); } } } } if (!defined(item.imageryLayer)) { return; } for (j = 0; j < n; ++j) { this.scene.imageryLayers.raise(item.imageryLayer); } }; /** * Lower an item's level in the viewer * This does not check that index is valid * @param {Number} index The index of the item to lower */ Cesium.prototype.lower = function(index) { var items = this.terria.nowViewing.items; var item = items[index]; var itemBelow = items[index + 1]; if (!defined(itemBelow.items) && !defined(itemBelow.imageryLayer)) { return; } // same considerations as above, but lower composite subitems starting at the other end of the list var n = countNumberOfSubItems(itemBelow); var i, j, subItem; if (defined(item.items)) { for (i = 0; i < item.items.length; ++i) { subItem = item.items[i]; if (defined(subItem.imageryLayer)) { for (j = 0; j < n; ++j) { this.scene.imageryLayers.lower(subItem.imageryLayer); } } } } if (!defined(item.imageryLayer)) { return; } for (j = 0; j < n; ++j) { this.scene.imageryLayers.lower(item.imageryLayer); } }; /** * Lowers this imagery layer to the bottom, underneath all other layers. If this item is not enabled or not shown, * this method does nothing. * @param {CatalogItem} item The item to lower to the bottom (usually a basemap) */ Cesium.prototype.lowerToBottom = function(item) { if (defined(item.items)) { // the front item is at the end of the list. // so to preserve order of any subitems, send any subitems to the bottom in order from the front for (var i = item.items.length - 1; i >= 0; --i) { var subItem = item.items[i]; this.lowerToBottom(subItem); // recursive } } if (!defined(item._imageryLayer)) { return; } this.terria.cesium.scene.imageryLayers.lowerToBottom(item._imageryLayer); }; Cesium.prototype.adjustDisclaimer = function() { }; /** * Picks features based off a latitude, longitude and (optionally) height. * @param {Object} latlng The position on the earth to pick. * @param {Object} imageryLayerCoords A map of imagery provider urls to the coords used to get features for those imagery * providers - i.e. x, y, level * @param existingFeatures An optional list of existing features to concatenate the ones found from asynchronous picking to. */ Cesium.prototype.pickFromLocation = function(latlng, imageryLayerCoords, existingFeatures) { var pickPosition = this.scene.globe.ellipsoid.cartographicToCartesian(Cartographic.fromDegrees(latlng.lng, latlng.lat, latlng.height)); var pickPositionCartographic = Ellipsoid.WGS84.cartesianToCartographic(pickPosition); var promises = []; var imageryLayers = []; for (var i = this.scene.imageryLayers.length - 1; i >= 0; i--) { var imageryLayer = this.scene.imageryLayers.get(i); var imageryProvider = imageryLayer._imageryProvider; if (imageryProvider.url && imageryLayerCoords[imageryProvider.url]) { var coords = imageryLayerCoords[imageryProvider.url]; promises.push(imageryProvider.pickFeatures(coords.x, coords.y, coords.level, pickPositionCartographic.longitude, pickPositionCartographic.latitude)); imageryLayers.push(imageryLayer); } } var result = this._buildPickedFeatures(imageryLayerCoords, pickPosition, existingFeatures, promises, imageryLayers, pickPositionCartographic.height); var mapInteractionModeStack = this.terria.mapInteractionModeStack; if (defined(mapInteractionModeStack) && mapInteractionModeStack.length > 0) { mapInteractionModeStack[mapInteractionModeStack.length - 1].pickedFeatures = result; } else { this.terria.pickedFeatures = result; } }; /** * Picks features based on coordinates relative to the Cesium window. Will draw a ray from the camera through the point * specified and set terria.pickedFeatures based on this. * * @param {Cartesian3} screenPosition The position on the screen. */ Cesium.prototype.pickFromScreenPosition = function(screenPosition) { var pickRay = this.scene.camera.getPickRay(screenPosition); var pickPosition = this.scene.globe.pick(pickRay, this.scene); var pickPositionCartographic = Ellipsoid.WGS84.cartesianToCartographic(pickPosition); var vectorFeatures = this.pickVectorFeatures(screenPosition); var providerCoords = this._attachProviderCoordHooks(); var pickRasterPromise = this.scene.imageryLayers.pickImageryLayerFeatures(pickRay, this.scene); var result = this._buildPickedFeatures(providerCoords, pickPosition, vectorFeatures, [pickRasterPromise], undefined, pickPositionCartographic.height); var mapInteractionModeStack = this.terria.mapInteractionModeStack; if (defined(mapInteractionModeStack) && mapInteractionModeStack.length > 0) { mapInteractionModeStack[mapInteractionModeStack.length - 1].pickedFeatures = result; } else { this.terria.pickedFeatures = result; } }; /** * Picks all *vector* features (e.g. GeoJSON) shown at a certain position on the screen, ignoring raster features * (e.g. WFS). Because all vector features are already in memory, this is synchronous. * * @param {Cartesian2} screenPosition position on the screen to look for features * @returns {Feature[]} The features found. */ Cesium.prototype.pickVectorFeatures = function(screenPosition) { // Pick vector features var vectorFeatures = []; var picked = this.scene.drillPick(screenPosition); for (var i = 0; i < picked.length; ++i) { var id = picked[i].id; if (id && id.entityCollection && id.entityCollection.owner && id.entityCollection.owner.name === GlobeOrMap._featureHighlightName) { continue; } if (!defined(id) && defined(picked[i].primitive)) { id = picked[i].primitive.id; } if (id instanceof Entity && vectorFeatures.indexOf(id) === -1) { var feature = Feature.fromEntityCollectionOrEntity(id); vectorFeatures.push(feature); } } return vectorFeatures; }; /** * Hooks into the {@link ImageryProvider#pickFeatures} method of every imagery provider in the scene - when this method is * evaluated (usually as part of feature picking), it will record the tile coordinates used against the url of the * imagery provider in an object that is returned by this method. Hooks are removed immediately after being executed once. * * @returns {{x, y, level}} A map of urls to the coords used by the imagery provider when picking features. Will * initially be empty but will be updated as the hooks are evaluated. * @private */ Cesium.prototype._attachProviderCoordHooks = function() { var providerCoords = {}; var pickFeaturesHook = function(imageryProvider, oldPick, x, y, level, longitude, latitude) { var featuresPromise = oldPick.call(imageryProvider, x, y, level, longitude, latitude); // Use url to uniquely identify providers because what else can we do? if (imageryProvider.url) { providerCoords[imageryProvider.url] = { x: x, y: y, level: level }; } imageryProvider.pickFeatures = oldPick; return featuresPromise; }; for (var j = 0; j < this.scene.imageryLayers.length; j++) { var imageryProvider = this.scene.imageryLayers.get(j).imageryProvider; imageryProvider.pickFeatures = pickFeaturesHook.bind(undefined, imageryProvider, imageryProvider.pickFeatures); } return providerCoords; }; /** * Builds a {@link PickedFeatures} object from a number of inputs. * * @param {{x, y, level}} providerCoords A map of imagery provider urls to the coords used to get features for that provider. * @param {Cartesian3} pickPosition The position in the 3D model that has been picked. * @param {Entity[]} existingFeatures Existing features - the results of feature promises will be appended to this. * @param {Promise[]} featurePromises Zero or more promises that each resolve to a list of {@link ImageryLayerFeatureInfo}s * (usually there will be one promise per ImageryLayer. These will be combined as part of * {@link PickedFeatures#allFeaturesAvailablePromise} and their results used to build the final * {@link PickedFeatures#features} array. * @param {ImageryLayer[]} imageryLayers An array of ImageryLayers that should line up with the one passed as featurePromises. * @param {number} defaultHeight The height to use for feature position heights if none is available when picking. * @returns {PickedFeatures} A {@link PickedFeatures} object that is a combination of everything passed. * @private */ Cesium.prototype._buildPickedFeatures = function(providerCoords, pickPosition, existingFeatures, featurePromises, imageryLayers, defaultHeight) { var result = new PickedFeatures(); result.providerCoords = providerCoords; result.pickPosition = pickPosition; result.allFeaturesAvailablePromise = when.all(featurePromises).then(function(allFeatures) { result.isLoading = false; result.features = allFeatures.reduce(function(resultFeaturesSoFar, imageryLayerFeatures, i) { if (!defined(imageryLayerFeatures)) { return resultFeaturesSoFar; } return resultFeaturesSoFar.concat(imageryLayerFeatures.map(function(feature) { if (defined(imageryLayers)) { feature.imageryLayer = imageryLayers[i]; } if (!defined(feature.position)) { feature.position = Ellipsoid.WGS84.cartesianToCartographic(pickPosition); } // If the picked feature does not have a height, use the height of the picked location. // This at least avoids major parallax effects on the selection indicator. if (!defined(feature.position.height) || feature.position.height === 0.0) { feature.position.height = defaultHeight; } return this._createFeatureFromImageryLayerFeature(feature); }.bind(this))); }.bind(this), defaultValue(existingFeatures, [])); }.bind(this)).otherwise(function() { result.isLoading = false; result.error = 'An unknown error occurred while picking features.'; }); return result; }; /** * Returns a new layer using a provided ImageryProvider, and adds it to the scene. * Note the optional parameters are a superset of the Leaflet version of this function, with one deletion (onProjectionError). * * @param {Object} options Options * @param {ImageryProvider} options.imageryProvider The imagery provider to create a new layer for. * @param {Number} [layerIndex] The index to add the layer at. If omitted, the layer will added on top of all existing layers. * @param {Rectangle} [options.rectangle=imageryProvider.rectangle] The rectangle of the layer. This rectangle * can limit the visible portion of the imagery provider. * @param {Number|Function} [options.opacity=1.0] The alpha blending value of this layer, from 0.0 to 1.0. * This can either be a simple number or a function with the signature * <code>function(frameState, layer, x, y, level)</code>. The function is passed the * current frame state, this layer, and the x, y, and level coordinates of the * imagery tile for which the alpha is required, and it is expected to return * the alpha value to use for the tile. * @param {Boolean} [options.clipToRectangle] * @param {Boolean} [options.treat403AsError] * @param {Boolean} [options.treat403AsError] * @param {Boolean} [options.ignoreUnknownTileErrors] * @param {Function} [options.onLoadError] * @returns {ImageryLayer} The newly created layer. */ Cesium.prototype.addImageryProvider = function(options) { var scene = this.scene; var errorEvent = options.imageryProvider.errorEvent; if (defined(errorEvent)) { errorEvent.addEventListener(options.onLoadError); } var result = new ImageryLayer(options.imageryProvider, { show : false, alpha : options.opacity, rectangle : options.clipToRectangle ? options.rectangle : undefined, isRequired : options.isRequiredForRendering // TODO: This doesn't seem to be a valid option for ImageryLayer - remove (and upstream)? }); // layerIndex is an optional parameter used when the imageryLayer corresponds to a CsvCatalogItem whose selected item has just changed // to ensure that the layer is re-added in the correct position scene.imageryLayers.add(result, options.layerIndex); this.updateLayerOrderToKeepOnTop(); return result; }; Cesium.prototype.removeImageryLayer = function(options) { var scene = this.scene; scene.imageryLayers.remove(options.layer); }; Cesium.prototype.showImageryLayer = function(options) { options.layer.show = true; }; Cesium.prototype.hideImageryLayer = function(options) { options.layer.show = false; }; Cesium.prototype.isImageryLayerShown = function(options) { return options.layer.show; }; Cesium.prototype.updateItemForSplitter = function(item) { if (!defined(item.splitDirection) || !defined(item.imageryLayer)) { return; } const terria = item.terria; if (terria.showSplitter) { item.imageryLayer.splitDirection = item.splitDirection; } else { item.imageryLayer.splitDirection = ImagerySplitDirection.NONE; } // Also update the next layer, if any. if (item._nextLayer) { item._nextLayer.splitDirection = item.imageryLayer.splitDirection; } this.notifyRepaintRequired(); }; Cesium.prototype.pauseMapInteraction = function() { ++this._pauseMapInteractionCount; if (this._pauseMapInteractionCount === 1) { this.scene.screenSpaceCameraController.enableInputs = false; } }; Cesium.prototype.resumeMapInteraction = function() { --this._pauseMapInteractionCount; if (this._pauseMapInteractionCount === 0) { setTimeout(() => { if (this._pauseMapInteractionCount === 0) { this.scene.screenSpaceCameraController.enableInputs = true; } }, 0); } }; function postRender(cesium, date) { // We can safely stop rendering when: // - the camera position hasn't changed in over a second, // - there are no tiles waiting to load, and // - the clock is not animating // - there are no tweens in progress var now = getTimestamp(); var scene = cesium.scene; if (!Matrix4.equalsEpsilon(cesium._lastCameraViewMatrix, scene.camera.viewMatrix, 1e-5)) { cesium._lastCameraMoveTime = now; } var cameraMovedInLastSecond = now - cesium._lastCameraMoveTime < 1000; var surface = scene.globe._surface; var tilesWaiting = !surface._tileProvider.ready || surface._tileLoadQueueHigh.length > 0 || surface._tileLoadQueueMedium.length > 0 || surface._tileLoadQueueLow.length > 0 || surface._debug.tilesWaitingForChildren > 0; if (!cameraMovedInLastSecond && !tilesWaiting && !cesium.viewer.clock.shouldAnimate && cesium.scene.tweens.length === 0) { if (cesium.verboseRendering) { console.log('stopping rendering @ ' + getTimestamp()); } cesium.viewer.useDefaultRenderLoop = false; cesium.stoppedRendering = true; } Matrix4.clone(scene.camera.viewMatrix, cesium._lastCameraViewMatrix); var feature = cesium.terria.selectedFeature; if (defined(feature) && defined(feature.position)) { cesium._selectionIndicator.position = feature.position.getValue(cesium.terria.clock.currentTime); } cesium._selectionIndicator.update(); } function selectFeature(cesium) { var feature = cesium.terria.selectedFeature; cesium._highlightFeature(feature); if (defined(feature) && defined(feature.position)) { cesium._selectionIndicator.position = feature.position.getValue(cesium.terria.clock.currentTime); cesium._selectionIndicator.animateAppear(); } else { cesium._selectionIndicator.animateDepart(); } cesium._selectionIndicator.update(); } module.exports = Cesium;