"use strict";
/*global require*/
var L = require("leaflet");
var html2canvas = require("terriajs-html2canvas");
var Cartesian2 = require("terriajs-cesium/Source/Core/Cartesian2").default;
var Cartographic = require("terriajs-cesium/Source/Core/Cartographic").default;
var CesiumMath = require("terriajs-cesium/Source/Core/Math").default;
var CesiumTileLayer = require("../Map/CesiumTileLayer");
var MapboxVectorCanvasTileLayer = require("../Map/MapboxVectorCanvasTileLayer");
var MapboxVectorTileImageryProvider = require("../Map/MapboxVectorTileImageryProvider");
var defined = require("terriajs-cesium/Source/Core/defined").default;
var destroyObject = require("terriajs-cesium/Source/Core/destroyObject")
.default;
var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
.default;
var EasingFunction = require("terriajs-cesium/Source/Core/EasingFunction")
.default;
var Ellipsoid = require("terriajs-cesium/Source/Core/Ellipsoid").default;
var ImagerySplitDirection = require("terriajs-cesium/Source/Scene/ImagerySplitDirection")
.default;
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var Rectangle = require("terriajs-cesium/Source/Core/Rectangle").default;
var cesiumRequestAnimationFrame = require("terriajs-cesium/Source/Core/requestAnimationFrame")
.default;
var TweenCollection = require("terriajs-cesium/Source/Scene/TweenCollection")
.default;
var when = require("terriajs-cesium/Source/ThirdParty/when").default;
var defaultValue = require("terriajs-cesium/Source/Core/defaultValue").default;
var FeatureDetection = require("terriajs-cesium/Source/Core/FeatureDetection")
.default;
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;
/**
* Gets the {@link LeafletDataSourceDisplay} used to render a {@link DataSource}.
* @type {LeafletDataSourceDisplay}
*/
this.dataSourceDisplay = undefined;
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) {
// Handle click events that cross the anti-meridian
if (e.latlng.lng > 180 || e.latlng.lng < -180) e.latlng = e.latlng.wrap();
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|DataSource} target The view, extent or DataSource 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(target, flightDurationSeconds) {
if (!defined(target)) {
throw new DeveloperError("target is required.");
}
var that = this;
return when().then(function() {
var bounds;
// Target is a KML data source
if (defined(target.entities)) {
var dataSourceDisplay = that.dataSourceDisplay;
bounds = dataSourceDisplay.getLatLngBounds(target);
} else {
var extent;
if (target instanceof Rectangle) {
extent = target;
} else {
extent = target.rectangle;
}
// Account for a bounding box crossing the date line.
if (extent.east < extent.west) {
extent = Rectangle.clone(extent);
extent.east += CesiumMath.TWO_PI;
}
bounds = rectangleToLatLngBounds(extent);
}
if (defined(bounds)) {
that.map.flyToBounds(bounds, {
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 {Boolean} ignoreSplitter An optional arg to ignore the splitter and return all feature picking results. Defaults to false.
* @param existingFeatures An optional list of existing features to concatenate the ones found from asynchronous picking to.
*/
Leaflet.prototype.pickFromLocation = function(
latlng,
imageryLayerCoords,
existingFeatures,
ignoreSplitter
) {
pickFeatures(
this,
latlng,
imageryLayerCoords,
existingFeatures,
ignoreSplitter
);
};
/**
* 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";
}
item.imageryLayer.splitDirection = item.splitDirection;
// Also update the next layer, if any.
if (
item._nextLayer &&
item._nextLayer.getContainer &&
item._nextLayer.getContainer()
) {
item._nextLayer.getContainer().style.clip = container.style.clip;
item._nextLayer.splitDirection = item.imageryLayer.splitDirection;
}
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,
ignoreSplitter
) {
ignoreSplitter = defaultValue(ignoreSplitter, false);
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 (
leaflet.terria.allowFeatureInfoRequests &&
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
) {
if (leaflet.terria.showSplitter && !ignoreSplitter) {
// Skip unless the layer is on the picked side or belongs to both sides of the splitter
var screenPosition = leaflet.computePositionOnScreen(
pickedFeatures.pickPosition
);
var pickedSide = leaflet.terria.getSplitterSideForScreenPosition(
screenPosition
);
var splitDirection = result.imageryLayer.splitDirection;
if (
!(
splitDirection === pickedSide ||
splitDirection === ImagerySplitDirection.NONE
)
) {
return allFeatures;
}
}
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;