terriajs
Version:
Geospatial data visualization platform.
910 lines (778 loc) • 35.6 kB
JavaScript
'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;