"use strict";
/*global require*/
var Color = require("terriajs-cesium/Source/Core/Color").default;
var defined = require("terriajs-cesium/Source/Core/defined").default;
var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
.default;
var Ellipsoid = require("terriajs-cesium/Source/Core/Ellipsoid").default;
var featureDataToGeoJson = require("../Map/featureDataToGeoJson");
var clone = require("terriajs-cesium/Source/Core/clone").default;
var MapboxVectorTileImageryProvider = require("../Map/MapboxVectorTileImageryProvider");
var MapboxVectorCanvasTileLayer = require("../Map/MapboxVectorCanvasTileLayer");
var GeoJsonCatalogItem = require("./GeoJsonCatalogItem");
var Feature = require("./Feature");
var ImageryLayer = require("terriajs-cesium/Source/Scene/ImageryLayer").default;
var rectangleToLatLngBounds = require("../Map/rectangleToLatLngBounds");
require("./ImageryLayerFeatureInfo"); // overrides Cesium's prototype.configureDescriptionFromProperties
/**
* The base class for map/globe viewers.
*
* @constructor
* @alias GlobeOrMap
*
* @param {Terria} terria The Terria instance.
* @param {String} disclaimerClass Class of a disclaimer element that should be shifted upwards to make room for other ui elements.
*
* @see Cesium
* @see Leaflet
*/
var GlobeOrMap = function(terria) {
/**
* Gets or sets the Terria instance.
* @type {Terria}
*/
this.terria = terria;
/**
* Gets or sets whether this viewer _can_ show a splitter. Default false.
* @type {Boolean}
*/
this.canShowSplitter = false;
this._tilesLoadingCountMax = 0;
this._removeHighlightCallback = undefined;
this._highlightPromise = undefined;
};
GlobeOrMap._featureHighlightName = "___$FeatureHighlight&__";
/**
* Creates a {@see Feature} (based on an {@see Entity}) from a {@see ImageryLayerFeatureInfo}.
* @param {ImageryLayerFeatureInfo} imageryFeature The imagery layer feature for which to create an entity-based feature.
* @return {Feature} The created feature.
* @protected
*/
GlobeOrMap.prototype._createFeatureFromImageryLayerFeature = function(
imageryFeature
) {
var feature = new Feature({
id: imageryFeature.name
});
feature.name = imageryFeature.name;
feature.description = imageryFeature.description; // already defined by the new Entity
feature.properties = imageryFeature.properties;
feature.data = imageryFeature.data;
feature.imageryLayer = imageryFeature.imageryLayer;
feature.position = Ellipsoid.WGS84.cartographicToCartesian(
imageryFeature.position
);
feature.coords = imageryFeature.coords;
return feature;
};
GlobeOrMap.prototype.updateTilesLoadingCount = function(tilesLoadingCount) {
if (tilesLoadingCount > this._tilesLoadingCountMax) {
this._tilesLoadingCountMax = tilesLoadingCount;
} else if (tilesLoadingCount === 0) {
this._tilesLoadingCountMax = 0;
}
this.terria.tileLoadProgressEvent.raiseEvent(
tilesLoadingCount,
this._tilesLoadingCountMax
);
};
GlobeOrMap.prototype.isDestroyed = function() {
return false;
};
/**
* 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.
*/
GlobeOrMap.prototype.pickFromLocation = function(
latlng,
imageryLayerCoords,
existingFeatures
) {
throw new DeveloperError(
"pickFromLocation must be implemented in the derived class."
);
};
GlobeOrMap.prototype.destroy = function() {
throw new DeveloperError("destroy must be implemented in the derived class.");
};
/**
* 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.
*/
GlobeOrMap.prototype.getCurrentExtent = function() {
throw new DeveloperError(
"getCurrentExtent must be implemented in the derived class."
);
};
/**
* Gets the current container element.
* @return {Element} The current container element.
*/
GlobeOrMap.prototype.getContainer = function() {
throw new DeveloperError(
"getContainer must be implemented in the derived class."
);
};
/**
* Zooms to a specified camera view or extent with a smooth flight animation.
*
* @param {CameraView|Rectangle} viewOrExtent The view or extent to which to zoom.
* @param {Number} [flightDurationSeconds=3.0] The length of the flight animation in seconds.
*/
GlobeOrMap.prototype.zoomTo = function(viewOrExtent, flightDurationSeconds) {
throw new DeveloperError("zoomTo must be implemented in the derived class.");
};
/**
* Captures a screenshot of the map.
* @return {Promise<string>} A promise that resolves to a data URL when the screenshot is ready.
*/
GlobeOrMap.prototype.captureScreenshot = function() {
throw new DeveloperError(
"captureScreenshot must be implemented in the derived class."
);
};
/**
* Notifies the viewer that a repaint is required.
*/
GlobeOrMap.prototype.notifyRepaintRequired = function() {
throw new DeveloperError(
"notifyRepaintRequired must be implemented in the derived class."
);
};
/**
* 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.
*/
GlobeOrMap.prototype.computePositionOnScreen = function(position, result) {
throw new DeveloperError(
"computePositionOnScreen must be implemented in the derived class."
);
};
/**
* Adds an attribution to the globe or map.
* @param {Credit} attribution The attribution to add.
*/
GlobeOrMap.prototype.addAttribution = function(attribution) {
throw new DeveloperError(
"addAttribution must be implemented in the derived class."
);
};
/**
* Removes an attribution from the globe or map.
* @param {Credit} attribution The attribution to remove.
*/
GlobeOrMap.prototype.removeAttribution = function(attribution) {
throw new DeveloperError(
"removeAttribution must be implemented in the derived class."
);
};
/**
* Gets all attribution currently active on the globe or map.
* @returns {String[]} The list of current attributions, as HTML strings.
*/
GlobeOrMap.prototype.getAllAttribution = function() {
return [];
};
/**
* Perform any updates to the order of layers required by raise and lower,
* but after the items have been reordered.
* This allows for the possibility that raise and lower do nothing, and instead we
* call updateLayerOrder
*/
GlobeOrMap.prototype.updateLayerOrderAfterReorder = function() {
throw new DeveloperError(
"updateLayerOrderAfterReorder must be implemented in the derived class."
);
};
/**
* Raise an item's level in the viewer
* This does not check that index is valid
* @param {Number} index The index of the item to raise
*/
GlobeOrMap.prototype.raise = function(index) {
throw new DeveloperError("raise must be implemented in the derived class.");
};
/**
* Lower an item's level in the viewer
* This does not check that index is valid
* @param {Number} index The index of the item to lower
*/
GlobeOrMap.prototype.lower = function(index) {
throw new DeveloperError("lower must be implemented in the derived class.");
};
/**
* 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)
*/
GlobeOrMap.prototype.lowerToBottom = function(item) {
throw new DeveloperError(
"lowerToBottom must be implemented in the derived class."
);
};
GlobeOrMap.prototype._highlightFeature = function(feature) {
if (defined(this._removeHighlightCallback)) {
this._removeHighlightCallback();
this._removeHighlightCallback = undefined;
this._highlightPromise = undefined;
}
if (defined(feature)) {
var hasGeometry = false;
if (defined(feature.polygon)) {
hasGeometry = true;
var cesiumPolygon = feature.cesiumEntity || feature;
var polygonOutline = cesiumPolygon.polygon.outline;
var polygonOutlineColor = cesiumPolygon.polygon.outlineColor;
var polygonMaterial = cesiumPolygon.polygon.material;
cesiumPolygon.polygon.outline = true;
cesiumPolygon.polygon.outlineColor = Color.fromCssColorString(
this.terria.baseMapContrastColor
);
const currentColor = polygonMaterial.getValue().color;
cesiumPolygon.polygon.material = Color.fromCssColorString(
this.terria.baseMapContrastColor
).withAlpha(currentColor.alpha);
this._removeHighlightCallback = function() {
const highlightFeatureColor = cesiumPolygon.polygon.material.getValue()
.color;
cesiumPolygon.polygon.outline = polygonOutline;
cesiumPolygon.polygon.outlineColor = polygonOutlineColor
.getValue()
.withAlpha(highlightFeatureColor.alpha);
cesiumPolygon.polygon.material.color = currentColor.withAlpha(
highlightFeatureColor.alpha
);
};
}
if (defined(feature.polyline)) {
hasGeometry = true;
var cesiumPolyline = feature.cesiumEntity || feature;
var polylineMaterial = cesiumPolyline.polyline.material;
var polylineWidth = cesiumPolyline.polyline.width;
cesiumPolyline.polyline.material = Color.fromCssColorString(
this.terria.baseMapContrastColor
);
cesiumPolyline.polyline.width = 2;
this._removeHighlightCallback = function() {
cesiumPolyline.polyline.material = polylineMaterial;
cesiumPolyline.polyline.width = polylineWidth;
};
}
if (!hasGeometry) {
if (
feature.imageryLayer &&
feature.imageryLayer.imageryProvider instanceof
MapboxVectorTileImageryProvider
) {
if (defined(this.terria.cesium)) {
var result = new ImageryLayer(
feature.imageryLayer.imageryProvider.createHighlightImageryProvider(
feature.data.id
),
{
show: true,
alpha: 1
}
);
var scene = this.terria.cesium.scene;
scene.imageryLayers.add(result);
this._removeHighlightCallback = function() {
scene.imageryLayers.remove(result);
};
} else if (defined(this.terria.leaflet)) {
var map = this.terria.leaflet.map;
var options = {
async: true,
opacity: 1,
bounds: rectangleToLatLngBounds(
feature.imageryLayer.imageryProvider.rectangle
)
};
if (defined(map.options.maxZoom)) {
options.maxZoom = map.options.maxZoom;
}
var layer = new MapboxVectorCanvasTileLayer(
feature.imageryLayer.imageryProvider.createHighlightImageryProvider(
feature.data.id
),
options
);
layer.addTo(map);
layer.bringToFront();
this._removeHighlightCallback = function() {
map.removeLayer(layer);
};
}
} else if (
!defined(this.supportsPolylinesOnTerrain) ||
this.supportsPolylinesOnTerrain
) {
var geoJson = featureDataToGeoJson(feature.data);
// Show geometry associated with the feature.
// Don't show points; the targeting cursor is sufficient.
if (geoJson && geoJson.geometry && geoJson.geometry.type !== "Point") {
// Turn Polygons into MultiLineStrings, because we're only showing the outline.
if (
geoJson.geometry.type === "Polygon" ||
geoJson.geometry.type === "MultiPolygon"
) {
geoJson = clone(geoJson);
geoJson.geometry = clone(geoJson.geometry);
if (geoJson.geometry.type === "MultiPolygon") {
const newCoordinates = [];
geoJson.geometry.coordinates.forEach(polygon => {
newCoordinates.push(...polygon);
});
geoJson.geometry.coordinates = newCoordinates;
}
geoJson.geometry.type = "MultiLineString";
}
var catalogItem = new GeoJsonCatalogItem(this.terria);
catalogItem.name = GlobeOrMap._featureHighlightName;
catalogItem.data = geoJson;
catalogItem.clampToGround = true;
catalogItem.style = {
"stroke-width": 2,
stroke: this.terria.baseMapContrastColor,
"fill-opacity": 0,
"marker-color": this.terria.baseMapContrastColor
};
var that = this;
var removeCallback = (this._removeHighlightCallback = function() {
that._highlightPromise
.then(function() {
if (removeCallback !== that._removeHighlightCallback) {
return;
}
catalogItem._hide();
catalogItem._disable();
})
.otherwise(function() {});
});
that._highlightPromise = catalogItem.load().then(function() {
if (removeCallback !== that._removeHighlightCallback) {
return;
}
catalogItem._enable();
catalogItem._show();
});
}
}
}
}
};
GlobeOrMap.prototype.addImageryProvider = function(options) {
throw new DeveloperError(
"addImageryProvider must be implemented in the derived class."
);
};
GlobeOrMap.prototype.removeImageryLayer = function(options) {
throw new DeveloperError(
"removeImageryLayer must be implemented in the derived class."
);
};
GlobeOrMap.prototype.showImageryLayer = function(options) {
throw new DeveloperError(
"showImageryLayer must be implemented in the derived class."
);
};
GlobeOrMap.prototype.hideImageryLayer = function(options) {
throw new DeveloperError(
"hideImageryLayer must be implemented in the derived class."
);
};
GlobeOrMap.prototype.isImageryLayerShown = function(options) {
throw new DeveloperError(
"isImageryLayerShown must be implemented in the derived class."
);
};
GlobeOrMap.prototype.addDataSource = function(options) {
this.terria.dataSources.add(options.dataSource);
};
GlobeOrMap.prototype.removeDataSource = function(options) {
this.terria.dataSources.remove(options.dataSource, false);
};
GlobeOrMap.prototype.updateAllItemsForSplitter = function() {
this.terria.nowViewing.items.forEach(item => {
this.updateItemForSplitter(item);
});
this.notifyRepaintRequired();
};
GlobeOrMap.prototype.updateItemForSplitter = function(item) {};
GlobeOrMap.prototype.pauseMapInteraction = function() {};
GlobeOrMap.prototype.resumeMapInteraction = function() {};
GlobeOrMap.disposeCommonListeners = function(globeOrMap) {
if (defined(globeOrMap._removeHighlightCallback)) {
globeOrMap._removeHighlightCallback();
globeOrMap._removeHighlightCallback = undefined;
globeOrMap._highlightPromise = undefined;
}
if (defined(globeOrMap._disclaimerShiftSubscription)) {
globeOrMap._disclaimerShiftSubscription.dispose();
globeOrMap._disclaimerShiftSubscription = undefined;
}
};
module.exports = GlobeOrMap;