UNPKG

terriajs

Version:

Geospatial data visualization platform.

910 lines (778 loc) 35.6 kB
'use strict'; /*global require*/ var L = require('leaflet'); var html2canvas = require('terriajs-html2canvas'); var Cartesian2 = require('terriajs-cesium/Source/Core/Cartesian2'); var Cartographic = require('terriajs-cesium/Source/Core/Cartographic'); var CesiumMath = require('terriajs-cesium/Source/Core/Math'); var CesiumTileLayer = require('../Map/CesiumTileLayer'); var MapboxVectorCanvasTileLayer = require('../Map/MapboxVectorCanvasTileLayer'); var MapboxVectorTileImageryProvider = require('../Map/MapboxVectorTileImageryProvider'); 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 EasingFunction = require('terriajs-cesium/Source/Core/EasingFunction'); var Ellipsoid = require('terriajs-cesium/Source/Core/Ellipsoid'); var ImagerySplitDirection = require('terriajs-cesium/Source/Scene/ImagerySplitDirection'); var knockout = require('terriajs-cesium/Source/ThirdParty/knockout'); var Rectangle = require('terriajs-cesium/Source/Core/Rectangle'); var cesiumRequestAnimationFrame = require('terriajs-cesium/Source/Core/requestAnimationFrame'); var TweenCollection = require('terriajs-cesium/Source/Scene/TweenCollection'); var when = require('terriajs-cesium/Source/ThirdParty/when'); var defaultValue = require('terriajs-cesium/Source/Core/defaultValue'); var FeatureDetection = require('terriajs-cesium/Source/Core/FeatureDetection'); var Feature = require('./Feature'); var GlobeOrMap = require('./GlobeOrMap'); var inherit = require('../Core/inherit'); var LeafletDragBox = require('../Map/LeafletDragBox'); var LeafletScene = require('../Map/LeafletScene'); var PickedFeatures = require('../Map/PickedFeatures'); var rectangleToLatLngBounds = require('../Map/rectangleToLatLngBounds'); var runLater = require('../Core/runLater'); const selectionIndicatorUrl = require('../../wwwroot/images/NM-LocationTarget.svg'); // Work around broken html2canvas 0.5.0-alpha2 window.html2canvas = html2canvas; LeafletDragBox.initialize(L); // Monkey patch this fix into L.Canvas: // https://github.com/Leaflet/Leaflet/pull/6033 // This is needed as of Leaflet 1.3.1, but will not be needed in the next version. const originalDestroyContainer = L.Canvas.prototype._destroyContainer; L.Canvas.prototype._destroyContainer = function() { L.Util.cancelAnimFrame(this._redrawRequest); originalDestroyContainer.apply(this, arguments); }; // Function taken from Leaflet 1.0.1 (https://github.com/Leaflet/Leaflet/blob/v1.0.1/src/layer/vector/Canvas.js#L254-L267) // Leaflet 1.0.2 and later don't trigger click events for every Path, so feature selection only gives 1 result. // Updated to incorporate function changes up to v1.3.1 L.Canvas.prototype._onClick = function (e) { var point = this._map.mouseEventToLayerPoint(e), layers = [], layer; for (var order = this._drawFirst; order; order = order.next) { layer = order.layer; if (layer.options.interactive && layer._containsPoint(point) && !this._map._draggableMoved(layer)) { L.DomEvent.fakeStop(e); layers.push(layer); } } if (layers.length) { this._fireEvent(layers, e); } }; /** * The Leaflet viewer component * * @alias Leaflet * @constructor * @extends GlobeOrMap * * @param {Terria} terria The Terria instance. * @param {Map} map The leaflet map instance. */ var Leaflet = function(terria, map) { GlobeOrMap.call(this, terria); /** * Gets or sets the Leaflet {@link Map} instance. * @type {Map} */ this.map = map; this.scene = new LeafletScene(map); /** * Gets or sets whether this viewer _can_ show a splitter. * @type {Boolean} */ this.canShowSplitter = true; this._tweens = new TweenCollection(); this._tweensAreRunning = false; this._selectionIndicatorTween = undefined; this._selectionIndicatorIsAppearing = undefined; this._pickedFeatures = undefined; this._selectionIndicator = L.marker([0, 0], { icon: L.divIcon({ className: '', html: '<img src="' + selectionIndicatorUrl + '" width="50" height="50" alt="" />', iconSize: L.point(50, 50) }), zIndexOffset: 1, // We increment the z index so that the selection marker appears above the item. interactive: false, keyboard: false }); this._selectionIndicator.addTo(this.map); this._selectionIndicatorDomElement = this._selectionIndicator._icon.children[0]; this._dragboxcompleted = false; this._pauseMapInteractionCount = 0; this.scene.featureClicked.addEventListener(featurePicked.bind(undefined, this)); var that = this; // if we receive dragboxend (see LeafletDragBox) and we are currently // accepting a rectangle, then return the box as the picked feature map.on('dragboxend', function(e) { var mapInteractionModeStack = that.terria.mapInteractionModeStack; if (defined(mapInteractionModeStack) && mapInteractionModeStack.length > 0) { if (mapInteractionModeStack[mapInteractionModeStack.length - 1].drawRectangle && defined(e.dragBoxBounds)) { var b = e.dragBoxBounds; mapInteractionModeStack[mapInteractionModeStack.length - 1].pickedFeatures = Rectangle.fromDegrees(b.getWest(), b.getSouth(), b.getEast(), b.getNorth()); } } that._dragboxcompleted = true; }); map.on('click', function(e) { if (!that._dragboxcompleted && that.map.dragging.enabled()) { pickFeatures(that, e.latlng); } that._dragboxcompleted = false; }); this._selectedFeatureSubscription = knockout.getObservable(this.terria, 'selectedFeature').subscribe(function() { selectFeature(this); }, this); this._splitterPositionSubscription = knockout.getObservable(this.terria, 'splitPosition').subscribe(function() { this.updateAllItemsForSplitter(); }, this); this._showSplitterSubscription = knockout.getObservable(terria, 'showSplitter').subscribe(function() { this.updateAllItemsForSplitter(); }, this); map.on('layeradd', function(e) { that.updateAllItemsForSplitter(); }); map.on('move', function(e) { that.updateAllItemsForSplitter(); }); this._initProgressEvent(); selectFeature(this); }; inherit(GlobeOrMap, Leaflet); Leaflet.prototype._initProgressEvent = function() { var onTileLoadChange = function() { var tilesLoadingCount = 0; this.map.eachLayer(function(layer) { if (layer._tiles) { // Count all tiles not marked as loaded tilesLoadingCount += Object.keys(layer._tiles).filter(key => !layer._tiles[key].loaded).length; } }); this.updateTilesLoadingCount(tilesLoadingCount); }.bind(this); this.map.on('layeradd', function(evt) { // This check makes sure we only watch tile layers, and also protects us if this private variable gets changed. if (typeof evt.layer._tiles !== 'undefined') { evt.layer.on('tileloadstart tileload load', onTileLoadChange); } }.bind(this)); this.map.on('layerremove', function(evt) { evt.layer.off('tileloadstart tileload load', onTileLoadChange); }.bind(this)); }; Leaflet.prototype.destroy = function() { 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.map.clearAllEventListeners(); this.map.eachLayer(layer => layer.clearAllEventListeners()); GlobeOrMap.disposeCommonListeners(this); return destroyObject(this); }; /** * 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. */ Leaflet.prototype.getCurrentExtent = function() { var bounds = this.map.getBounds(); return Rectangle.fromDegrees(bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()); }; /** * Gets the current container element. * @return {Element} The current container element. */ Leaflet.prototype.getContainer = function() { return this.map.getContainer(); }; /** * Zooms to a specified camera view or extent. * * @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. Leaflet ignores the actual value, * but will use an animated transition when this value is greater than 0. */ Leaflet.prototype.zoomTo = function(viewOrExtent, flightDurationSeconds) { if (!defined(viewOrExtent)) { throw new DeveloperError('viewOrExtent is required.'); } var extent; if (viewOrExtent instanceof Rectangle) { extent = viewOrExtent; } else { extent = viewOrExtent.rectangle; } // Account for a bounding box crossing the date line. if (extent.east < extent.west) { extent = Rectangle.clone(extent); extent.east += CesiumMath.TWO_PI; } this.map.flyToBounds(rectangleToLatLngBounds(extent), { animate: flightDurationSeconds > 0.0, duration: flightDurationSeconds }); }; function isSplitterDragThumb(element) { return element.className && element.className.indexOf && element.className.indexOf('tjs-splitter__thumb') >= 0; } /** * Captures a screenshot of the map. * @return {Promise} A promise that resolves to a data URL when the screenshot is ready. */ Leaflet.prototype.captureScreenshot = function() { // Temporarily hide the map credits. this.map.attributionControl.remove(); var that = this; let restoreLeft; let restoreRight; try { // html2canvas can't handle the clip style which is used for the splitter. So if the splitter is active, we render // a left image and a right image and compose them. Also remove the splitter drag thumb. let promise; if (this.terria.showSplitter) { const clips = getClipsForSplitter(this); const clipLeft = clips.left.replace(/ /g, ''); const clipRight = clips.right.replace(/ /g, ''); promise = html2canvas(this.map.getContainer(), { useCORS: true, ignoreElements: element => element.style && element.style.clip.replace(/ /g, '') === clipRight || isSplitterDragThumb(element) }).then(leftCanvas => { return html2canvas(this.map.getContainer(), { useCORS: true, ignoreElements: element => element.style && element.style.clip.replace(/ /g, '') === clipLeft || isSplitterDragThumb(element) }).then(rightCanvas => { const combined = document.createElement('canvas'); combined.width = leftCanvas.width; combined.height = leftCanvas.height; const context = combined.getContext('2d'); const split = clips.clipPositionWithinMap * window.devicePixelRatio; context.drawImage(leftCanvas, 0, 0, split, combined.height, 0, 0, split, combined.height); context.drawImage(rightCanvas, split, 0, combined.width - split, combined.height, split, 0, combined.width - split, combined.height); return combined; }); }); } else { promise = html2canvas(this.map.getContainer(), { useCORS: true }); } return when(promise).then(function(canvas) { return canvas.toDataURL("image/png"); }).always(function(v) { that.map.attributionControl.addTo(that.map); if (restoreLeft) { restoreLeft(); } if (restoreRight) { restoreRight(); } return v; }); } catch (e) { that.map.attributionControl.addTo(that.map); if (restoreLeft) { restoreLeft(); } if (restoreRight) { restoreRight(); } return when.reject(e); } }; /** * Notifies the viewer that a repaint is required. */ Leaflet.prototype.notifyRepaintRequired = function() { // Leaflet doesn't need to do anything with this notification. }; var cartographicScratch = new Cartographic(); /** * 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. */ Leaflet.prototype.computePositionOnScreen = function(position, result) { var cartographic = Ellipsoid.WGS84.cartesianToCartographic(position, cartographicScratch); var point = this.map.latLngToContainerPoint(L.latLng(CesiumMath.toDegrees(cartographic.latitude), CesiumMath.toDegrees(cartographic.longitude))); if (defined(result)) { result.x = point.x; result.y = point.y; } else { result = new Cartesian2(point.x, point.y); } return result; }; /** * Adds an attribution to the map. * @param {Credit} attribution The attribution to add. */ Leaflet.prototype.addAttribution = function(attribution) { if (attribution) { this.map.attributionControl.addAttribution(createLeafletCredit(attribution)); } }; /** * Removes an attribution from the map. * @param {Credit} attribution The attribution to remove. */ Leaflet.prototype.removeAttribution = function(attribution) { if (attribution) { this.map.attributionControl.removeAttribution(createLeafletCredit(attribution)); } }; /** * Gets all attribution currently active on the globe or map. * @returns {String[]} The list of current attributions, as HTML strings. */ Leaflet.prototype.getAllAttribution = function() { return Object.keys(this.map.attributionControl._attributions); }; // this private function is called by updateLayerOrder function updateOneLayer(item, currZIndex) { if (defined(item.imageryLayer) && defined(item.imageryLayer.setZIndex)) { if (item.supportsReordering) { item.imageryLayer.setZIndex(currZIndex.reorderable++); } else { item.imageryLayer.setZIndex(currZIndex.fixed++); } } } /** * Updates the order of layers on the Leaflet map to match the order in the Now Viewing pane. */ Leaflet.prototype.updateLayerOrder = function() { // Set the current z-index of all layers. var items = this.terria.nowViewing.items; var currZIndex = { reorderable: 100, // an arbitrary place to start fixed: 1000000 // fixed layers go on top of reorderable ones }; var i, j, currentItem, subItem; for (i = items.length - 1; i >= 0; --i) { currentItem = items[i]; if (defined(currentItem.items)) { for (j = currentItem.items.length - 1; j >= 0; --j) { subItem = currentItem.items[j]; updateOneLayer(subItem, currZIndex); } } updateOneLayer(currentItem, currZIndex); } }; /** * Because Leaflet doesn't actually do raise/lower, just reset the orders after every raise/lower */ Leaflet.prototype.updateLayerOrderAfterReorder = function() { this.updateLayerOrder(); }; Leaflet.prototype.raise = function(index) { // raising and lowering is instead handled by updateLayerOrderAfterReorder }; Leaflet.prototype.lower = function(index) { // raising and lowering is instead handled by updateLayerOrderAfterReorder }; /** * 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) */ Leaflet.prototype.lowerToBottom = function(item) { if (defined(item.items)) { for (var i = item.items.length - 1; i >= 0; --i) { var subItem = item.items[i]; this.lowerToBottom(subItem); // recursive } } if (!defined(item._imageryLayer)) { return; } item._imageryLayer.setZIndex(0); }; /** * 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. */ Leaflet.prototype.pickFromLocation = function(latlng, imageryLayerCoords, existingFeatures) { pickFeatures(this, latlng, imageryLayerCoords, existingFeatures); }; /** * Returns a new layer using a provided ImageryProvider. * Does not add it to anything - in Leaflet there is no equivalent to Cesium's ability to add a layer without showing it, * so here this is done by show/hide. * Note the optional parameters are a subset of the Cesium version of this function, with one addition (onProjectionError). * * @param {Object} options Options * @param {ImageryProvider} options.imageryProvider The imagery provider to create a new layer for. * @param {Rectangle} [options.rectangle=imageryProvider.rectangle] The rectangle of the layer. This rectangle * can limit the visible portion of the imagery provider. * @param {Number} [options.opacity=1.0] The alpha blending value of this layer, from 0.0 to 1.0. * @param {Boolean} [options.clipToRectangle] * @param {Function} [options.onLoadError] * @param {Function} [options.onProjectionError] * @returns {ImageryLayer} The newly created layer. */ Leaflet.prototype.addImageryProvider = function(options) { var layerOptions = { opacity: options.opacity, bounds : options.clipToRectangle && options.rectangle ? rectangleToLatLngBounds(options.rectangle) : undefined }; if (defined(this.map.options.maxZoom)) { layerOptions.maxZoom = this.map.options.maxZoom; } var result; if (options.imageryProvider instanceof MapboxVectorTileImageryProvider) { layerOptions.async = true; layerOptions.bounds = rectangleToLatLngBounds(options.imageryProvider.rectangle); result = new MapboxVectorCanvasTileLayer(options.imageryProvider, layerOptions); } else { result = new CesiumTileLayer(options.imageryProvider, layerOptions); } result.errorEvent.addEventListener(function(sender, message) { if (defined(options.onProjectionError)) { options.onProjectionError(); } // If the user re-shows the dataset, show the error again. result.initialized = false; }); var errorEvent = options.imageryProvider.errorEvent; if (defined(options.onLoadError) && defined(errorEvent)) { errorEvent.addEventListener(options.onLoadError); } return result; }; Leaflet.prototype.removeImageryLayer = function(options) { var map = this.map; // Comment - Leaflet.prototype.addImageryProvider doesn't add the layer to the map, // so it seems inconsistent that removeImageryLayer removes it. // (In contrast, Cesium.prototype.addImageryProvider does add it to the scene, and removeImageryLayer removes it from the scene). map.removeLayer(options.layer); }; Leaflet.prototype.showImageryLayer = function(options) { if (!this.map.hasLayer(options.layer)) { this.map.addLayer(options.layer); // Identical to layer.addTo(this.map), as Leaflet's L.layer.addTo(map) just calls map.addLayer. } this.updateLayerOrder(); }; Leaflet.prototype.hideImageryLayer = function(options) { this.map.removeLayer(options.layer); }; Leaflet.prototype.isImageryLayerShown = function(options) { return this.map.hasLayer(options.layer); }; // As of Internet Explorer 11.483.15063.0 and Edge 40.15063.0.0 (EdgeHTML 15.15063) there is an apparent // bug in both browsers where setting the `clip` CSS style on our Leaflet layers does not consistently // cause the new clip to be applied. The change shows up in the DOM inspector, but it is not reflected // in the rendered view. You can reproduce it by adding a layer and toggling it between left/both/right // repeatedly, and you will quickly see it fail to update sometimes. Unfortunateely my attempts to // reproduce this in jsfiddle were unsuccessful, so presumably there is something unusual about our // setup. In any case, we do the usually-horrible thing here of detecting these browsers by their user // agent, and then work around the bug by hiding the DOM element, forcing it to updated by asking for // its bounding client rectangle, and then showing it again. There's a bit of a performance hit to // this, so we don't do it on other browsers that do not experience this bug. const useClipUpdateWorkaround = FeatureDetection.isInternetExplorer() || FeatureDetection.isEdge(); Leaflet.prototype.updateItemForSplitter = function(item, clips) { if (!defined(item.splitDirection) || !defined(item.imageryLayer)) { return; } const layer = item.imageryLayer; const container = layer.getContainer && layer.getContainer(); if (!container) { return; } const { left: clipLeft, right: clipRight } = clips || getClipsForSplitter(this); if (container) { let display; if (useClipUpdateWorkaround) { display = container.style.display; container.style.display = 'none'; container.getBoundingClientRect(); } if (item.splitDirection === ImagerySplitDirection.LEFT) { container.style.clip = clipLeft; } else if (item.splitDirection === ImagerySplitDirection.RIGHT) { container.style.clip = clipRight; } else { container.style.clip = 'auto'; } // Also update the next layer, if any. if (item._nextLayer && item._nextLayer.getContainer && item._nextLayer.getContainer()) { item._nextLayer.getContainer().style.clip = container.style.clip; } if (useClipUpdateWorkaround) { container.style.display = display; } } }; Leaflet.prototype.updateAllItemsForSplitter = function() { const clips = getClipsForSplitter(this); this.terria.nowViewing.items.forEach(item => { this.updateItemForSplitter(item, clips); }); }; Leaflet.prototype.pauseMapInteraction = function() { ++this._pauseMapInteractionCount; if (this._pauseMapInteractionCount === 1) { this.map.dragging.disable(); } }; Leaflet.prototype.resumeMapInteraction = function() { --this._pauseMapInteractionCount; if (this._pauseMapInteractionCount === 0) { setTimeout(() => { if (this._pauseMapInteractionCount === 0) { this.map.dragging.enable(); } }, 0); } }; function getClipsForSplitter(viewer) { let clipLeft = ''; let clipRight = ''; let clipPositionWithinMap; let clipX; if (viewer.terria.showSplitter) { const map = viewer.map; const size = map.getSize(); const nw = map.containerPointToLayerPoint([0, 0]); const se = map.containerPointToLayerPoint(size); clipPositionWithinMap = size.x * viewer.terria.splitPosition; clipX = Math.round(nw.x + clipPositionWithinMap); clipLeft = 'rect(' + [nw.y, clipX, se.y, nw.x].join('px,') + 'px)'; clipRight = 'rect(' + [nw.y, se.x, se.y, clipX].join('px,') + 'px)'; } return { left: clipLeft, right: clipRight, clipPositionWithinMap: clipPositionWithinMap, clipX: clipX }; } /** * A convenient function for handling leaflet credit display * @param {Credit} attribution the original attribution object for leaflet to display as text or link * @return {String} The sanitized HTML for the credit. */ function createLeafletCredit(attribution) { return attribution.element; } /* * There are two "listeners" for clicks which are set up in our constructor. * - One fires for any click: `map.on('click', ...`. It calls `pickFeatures`. * - One fires only for vector features: `this.scene.featureClicked.addEventListener`. * It calls `featurePicked`, which calls `pickFeatures` and then adds the feature it found, if any. * These events can fire in either order. * Billboards do not fire the first event. * * Note that `pickFeatures` does nothing if `leaflet._pickedFeatures` is already set. * Otherwise, it sets it, runs `runLater` to clear it, and starts the asynchronous raster feature picking. * * So: * If only the first event is received, it triggers the raster-feature picking as desired. * If both are received in the order above, the second adds the vector features to the list of raster features as desired. * If both are received in the reverse order, the vector-feature click kicks off the same behavior as the other click would have; * and when the next click is received, it is ignored - again, as desired. */ function featurePicked(leaflet, entity, event) { pickFeatures(leaflet, event.latlng); // Ignore clicks on the feature highlight. if (entity && entity.entityCollection && entity.entityCollection.owner && entity.entityCollection.owner.name === GlobeOrMap._featureHighlightName) { return; } var feature = Feature.fromEntityCollectionOrEntity(entity); leaflet._pickedFeatures.features.push(feature); if (entity.position) { leaflet._pickedFeatures.pickPosition = entity.position._value; } } function pickFeatures(leaflet, latlng, tileCoordinates, existingFeatures) { if (defined(leaflet._pickedFeatures)) { // Picking is already in progress. return; } leaflet._pickedFeatures = new PickedFeatures(); if (defined(existingFeatures)) { leaflet._pickedFeatures.features = existingFeatures; } // We run this later because vector click events and the map click event can come through in any order, but we can // be reasonably sure that all of them will be processed by the time our runLater func is invoked. var cleanup = runLater(function() { // Set this again just in case a vector pick came through and reset it to the vector's position. var newPickLocation = Ellipsoid.WGS84.cartographicToCartesian(pickedLocation); var mapInteractionModeStack = leaflet.terria.mapInteractionModeStack; if (defined(mapInteractionModeStack) && mapInteractionModeStack.length > 0) { mapInteractionModeStack[mapInteractionModeStack.length - 1].pickedFeatures.pickPosition = newPickLocation; } else if (defined(leaflet.terria.pickedFeatures)) { leaflet.terria.pickedFeatures.pickPosition = newPickLocation; } // Unset this so that the next click will start building features from scratch. leaflet._pickedFeatures = undefined; }); var activeItems = leaflet.terria.nowViewing.items; tileCoordinates = defaultValue(tileCoordinates, {}); var pickedLocation = Cartographic.fromDegrees(latlng.lng, latlng.lat); leaflet._pickedFeatures.pickPosition = Ellipsoid.WGS84.cartographicToCartesian(pickedLocation); // We want the all available promise to return after the cleanup one to make sure all vector click events have resolved. var promises = [cleanup].concat(activeItems.filter(function(item) { return item.isEnabled && item.isShown && defined(item.imageryLayer) && defined(item.imageryLayer.pickFeatures); }).map(function(item) { var imageryLayerUrl = item.imageryLayer.imageryProvider.url; var longRadians = CesiumMath.toRadians(latlng.lng); var latRadians = CesiumMath.toRadians(latlng.lat); return when(tileCoordinates[imageryLayerUrl] || item.imageryLayer.getFeaturePickingCoords(leaflet.map, longRadians, latRadians)) .then(function(coords) { return item.imageryLayer.pickFeatures(coords.x, coords.y, coords.level, longRadians, latRadians).then(function(features) { return { features: features, imageryLayer: item.imageryLayer, coords: coords }; }); }); })); var pickedFeatures = leaflet._pickedFeatures; pickedFeatures.allFeaturesAvailablePromise = when.all(promises).then(function(results) { // Get rid of the cleanup promise var promiseResult = results.slice(1); pickedFeatures.isLoading = false; pickedFeatures.providerCoords = {}; var filteredResults = promiseResult.filter(function(result) { return defined(result.features) && result.features.length > 0; }); pickedFeatures.providerCoords = filteredResults.reduce(function(coordsSoFar, result) { coordsSoFar[result.imageryLayer.imageryProvider.url] = result.coords; return coordsSoFar; }, {}); pickedFeatures.features = filteredResults.reduce(function(allFeatures, result) { return allFeatures.concat(result.features.map(function(feature) { feature.imageryLayer = result.imageryLayer; // For features without a position, use the picked location. if (!defined(feature.position)) { feature.position = pickedLocation; } return leaflet._createFeatureFromImageryLayerFeature(feature); })); }, pickedFeatures.features); }).otherwise(function(e) { pickedFeatures.isLoading = false; pickedFeatures.error = 'An unknown error occurred while picking features.'; throw e; }); var mapInteractionModeStack = leaflet.terria.mapInteractionModeStack; if (defined(mapInteractionModeStack) && mapInteractionModeStack.length > 0) { mapInteractionModeStack[mapInteractionModeStack.length - 1].pickedFeatures = leaflet._pickedFeatures; } else { leaflet.terria.pickedFeatures = leaflet._pickedFeatures; } } function selectFeature(leaflet) { var feature = leaflet.terria.selectedFeature; leaflet._highlightFeature(feature); if (defined(feature) && defined(feature.position)) { var cartographic = Ellipsoid.WGS84.cartesianToCartographic(feature.position.getValue(leaflet.terria.clock.currentTime), cartographicScratch); leaflet._selectionIndicator.setLatLng([CesiumMath.toDegrees(cartographic.latitude), CesiumMath.toDegrees(cartographic.longitude)]); animateSelectionIndicatorAppear(leaflet); } else { animateSelectionIndicatorDepart(leaflet); } } function startTweens(leaflet) { if (leaflet._tweensAreRunning) { return; } var feature = leaflet.terria.selectedFeature; if (defined(feature) && defined(feature.position)) { var cartographic = Ellipsoid.WGS84.cartesianToCartographic(feature.position.getValue(leaflet.terria.clock.currentTime), cartographicScratch); leaflet._selectionIndicator.setLatLng([CesiumMath.toDegrees(cartographic.latitude), CesiumMath.toDegrees(cartographic.longitude)]); } if (leaflet._tweens.length > 0) { leaflet._tweens.update(); } if (leaflet._tweens.length !== 0 || (defined(feature) && defined(feature.position))) { cesiumRequestAnimationFrame(startTweens.bind(undefined, leaflet)); } } function animateSelectionIndicatorAppear(leaflet) { if (defined(leaflet._selectionIndicatorTween)) { if (leaflet._selectionIndicatorIsAppearing) { // Already appearing; don't restart the animation. return; } leaflet._selectionIndicatorTween.cancelTween(); leaflet._selectionIndicatorTween = undefined; } var style = leaflet._selectionIndicatorDomElement.style; leaflet._selectionIndicatorIsAppearing = true; leaflet._selectionIndicatorTween = leaflet._tweens.add({ startObject: { scale: 2.0, opacity: 0.0, rotate: -180 }, stopObject: { scale: 1.0, opacity: 1.0, rotate: 0 }, duration: 0.8, easingFunction: EasingFunction.EXPONENTIAL_OUT, update: function(value) { style.opacity = value.opacity; style.transform = 'scale(' + (value.scale) + ') rotate(' + value.rotate + 'deg)'; }, complete: function() { leaflet._selectionIndicatorTween = undefined; }, cancel: function() { leaflet._selectionIndicatorTween = undefined; } }); startTweens(leaflet); } function animateSelectionIndicatorDepart(leaflet) { if (defined(leaflet._selectionIndicatorTween)) { if (!leaflet._selectionIndicatorIsAppearing) { // Already disappearing, dont' restart the animation. return; } leaflet._selectionIndicatorTween.cancelTween(); leaflet._selectionIndicatorTween = undefined; } var style = leaflet._selectionIndicatorDomElement.style; leaflet._selectionIndicatorIsAppearing = false; leaflet._selectionIndicatorTween = leaflet._tweens.add({ startObject: { scale: 1.0, opacity: 1.0 }, stopObject: { scale: 1.5, opacity: 0.0 }, duration: 0.8, easingFunction: EasingFunction.EXPONENTIAL_OUT, update: function(value) { style.opacity = value.opacity; style.transform = 'scale(' + value.scale + ') rotate(0deg)'; }, complete: function() { leaflet._selectionIndicatorTween = undefined; }, cancel: function() { leaflet._selectionIndicatorTween = undefined; } }); startTweens(leaflet); } module.exports = Leaflet;