ol-cesium
Version:
OpenLayers Cesium integration library
1,315 lines (1,184 loc) • 44.7 kB
JavaScript
/**
* @module olcs.FeatureConverter
*/
import olGeomGeometry from 'ol/geom/Geometry.js';
import olStyleIcon from 'ol/style/Icon.js';
import olSourceVector from 'ol/source/Vector.js';
import olSourceCluster from 'ol/source/Cluster.js';
import {circular as olCreateCircularPolygon} from 'ol/geom/Polygon.js';
import {boundingExtent, getCenter} from 'ol/extent.js';
import olGeomSimpleGeometry from 'ol/geom/SimpleGeometry.js';
import olcsCore from './core.js';
import olcsCoreVectorLayerCounterpart from './core/VectorLayerCounterpart.js';
import olcsUtil, {getUid, isGroundPolylinePrimitiveSupported} from './util.js';
/**
* @typedef {Object} ModelStyle
* @property {Cesium.Matrix4} [debugModelMatrix]
* @property {Cesium.ModelFromGltfOptions} cesiumOptions
*/
class FeatureConverter {
/**
* Concrete base class for converting from OpenLayers3 vectors to Cesium
* primitives.
* Extending this class is possible provided that the extending class and
* the library are compiled together by the closure compiler.
* @param {!Cesium.Scene} scene Cesium scene.
* @constructor
* @api
*/
constructor(scene) {
/**
* @protected
*/
this.scene = scene;
/**
* Bind once to have a unique function for using as a listener
* @type {function(ol.source.Vector.Event)}
* @private
*/
this.boundOnRemoveOrClearFeatureListener_ = this.onRemoveOrClearFeature_.bind(this);
/**
* @type {Cesium.Cartesian3}
* @private
*/
this.defaultBillboardEyeOffset_ = new Cesium.Cartesian3(0, 0, 10);
}
/**
* @param {ol.source.Vector.Event} evt
* @private
*/
onRemoveOrClearFeature_(evt) {
const source = evt.target;
console.assert(source instanceof olSourceVector);
const cancellers = olcsUtil.obj(source)['olcs_cancellers'];
if (cancellers) {
const feature = evt.feature;
if (feature) {
// remove
const id = getUid(feature);
const canceller = cancellers[id];
if (canceller) {
canceller();
delete cancellers[id];
}
} else {
// clear
for (const key in cancellers) {
if (cancellers.hasOwnProperty(key)) {
cancellers[key]();
}
}
olcsUtil.obj(source)['olcs_cancellers'] = {};
}
}
}
/**
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature.
* @param {!Cesium.Primitive|Cesium.Label|Cesium.Billboard} primitive
* @protected
*/
setReferenceForPicking(layer, feature, primitive) {
primitive.olLayer = layer;
primitive.olFeature = feature;
}
/**
* Basics primitive creation using a color attribute.
* Note that Cesium has 'interior' and outline geometries.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature.
* @param {!ol.geom.Geometry} olGeometry OpenLayers geometry.
* @param {!Cesium.Geometry} geometry
* @param {!Cesium.Color} color
* @param {number=} opt_lineWidth
* @return {Cesium.Primitive}
* @protected
*/
createColoredPrimitive(layer, feature, olGeometry, geometry, color, opt_lineWidth) {
const createInstance = function(geometry, color) {
const instance = new Cesium.GeometryInstance({
// always update Cesium externs before adding a property
geometry
});
if (color && !(color instanceof Cesium.ImageMaterialProperty)) {
instance.attributes = {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(color)
};
}
return instance;
};
const options = {
// always update Cesium externs before adding a property
flat: true, // work with all geometries
renderState: {
depthTest: {
enabled: true
}
}
};
if (opt_lineWidth !== undefined) {
if (!options.renderState) {
options.renderState = {};
}
options.renderState.lineWidth = opt_lineWidth;
}
const instances = createInstance(geometry, color);
const heightReference = this.getHeightReference(layer, feature, olGeometry);
let primitive;
if (heightReference === Cesium.HeightReference.CLAMP_TO_GROUND) {
const ctor = instances.geometry.constructor;
if (ctor && !ctor['createShadowVolume']) {
return null;
}
primitive = new Cesium.GroundPrimitive({
geometryInstances: instances
});
} else {
primitive = new Cesium.Primitive({
geometryInstances: instances
});
}
if (color instanceof Cesium.ImageMaterialProperty) {
const dataUri = color.image.getValue().toDataURL();
primitive.appearance = new Cesium.MaterialAppearance({
flat: true,
renderState: {
depthTest: {
enabled: true
}
},
material: new Cesium.Material({
fabric: {
type: 'Image',
uniforms: {
image: dataUri
}
}
})
});
} else {
primitive.appearance = new Cesium.PerInstanceColorAppearance(options);
}
this.setReferenceForPicking(layer, feature, primitive);
return primitive;
}
/**
* Return the fill or stroke color from a plain ol style.
* @param {!ol.style.Style|ol.style.Text} style
* @param {boolean} outline
* @return {!Cesium.Color}
* @protected
*/
extractColorFromOlStyle(style, outline) {
const fillColor = style.getFill() ? style.getFill().getColor() : null;
const strokeColor = style.getStroke() ? style.getStroke().getColor() : null;
let olColor = 'black';
if (strokeColor && outline) {
olColor = strokeColor;
} else if (fillColor) {
olColor = fillColor;
}
return olcsCore.convertColorToCesium(olColor);
}
/**
* Return the width of stroke from a plain ol style.
* @param {!ol.style.Style|ol.style.Text} style
* @return {number}
* @protected
*/
extractLineWidthFromOlStyle(style) {
// Handling of line width WebGL limitations is handled by Cesium.
const width = style.getStroke() ? style.getStroke().getWidth() : undefined;
return width !== undefined ? width : 1;
}
/**
* Create a primitive collection out of two Cesium geometries.
* Only the OpenLayers style colors will be used.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature.
* @param {!ol.geom.Geometry} olGeometry OpenLayers geometry.
* @param {!Cesium.Geometry} fillGeometry
* @param {!Cesium.Geometry} outlineGeometry
* @param {!ol.style.Style} olStyle
* @return {!Cesium.PrimitiveCollection}
* @protected
*/
wrapFillAndOutlineGeometries(layer, feature, olGeometry, fillGeometry, outlineGeometry, olStyle) {
const fillColor = this.extractColorFromOlStyle(olStyle, false);
const outlineColor = this.extractColorFromOlStyle(olStyle, true);
const primitives = new Cesium.PrimitiveCollection();
if (olStyle.getFill()) {
const p1 = this.createColoredPrimitive(layer, feature, olGeometry,
fillGeometry, fillColor);
console.assert(!!p1);
primitives.add(p1);
}
if (olStyle.getStroke() && outlineGeometry) {
const width = this.extractLineWidthFromOlStyle(olStyle);
const p2 = this.createColoredPrimitive(layer, feature, olGeometry,
outlineGeometry, outlineColor, width);
if (p2) {
// Some outline geometries are not supported by Cesium in clamp to ground
// mode. These primitives are skipped.
primitives.add(p2);
}
}
return primitives;
}
// Geometry converters
/**
* Create a Cesium primitive if style has a text component.
* Eventually return a PrimitiveCollection including current primitive.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.Geometry} geometry
* @param {!ol.style.Style} style
* @param {!Cesium.Primitive} primitive current primitive
* @return {!Cesium.PrimitiveCollection}
* @protected
*/
addTextStyle(layer, feature, geometry, style, primitive) {
let primitives;
if (!(primitive instanceof Cesium.PrimitiveCollection)) {
primitives = new Cesium.PrimitiveCollection();
primitives.add(primitive);
} else {
primitives = primitive;
}
if (!style.getText()) {
return primitives;
}
const text = /** @type {!ol.style.Text} */ (style.getText());
const label = this.olGeometry4326TextPartToCesium(layer, feature, geometry,
text);
if (label) {
primitives.add(label);
}
return primitives;
}
/**
* Add a billboard to a Cesium.BillboardCollection.
* Overriding this wrapper allows manipulating the billboard options.
* @param {!Cesium.BillboardCollection} billboards
* @param {!Cesium.optionsBillboardCollectionAdd} bbOptions
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature.
* @param {!ol.geom.Geometry} geometry
* @param {!ol.style.Style} style
* @return {!Cesium.Billboard} newly created billboard
* @api
*/
csAddBillboard(billboards, bbOptions, layer, feature, geometry, style) {
if (!bbOptions.eyeOffset) {
bbOptions.eyeOffset = this.defaultBillboardEyeOffset_;
}
const bb = billboards.add(bbOptions);
this.setReferenceForPicking(layer, feature, bb);
return bb;
}
/**
* Convert an OpenLayers circle geometry to Cesium.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.Circle} olGeometry OpenLayers circle geometry.
* @param {!ol.ProjectionLike} projection
* @param {!ol.style.Style} olStyle
* @return {!Cesium.PrimitiveCollection} primitives
* @api
*/
olCircleGeometryToCesium(layer, feature, olGeometry, projection, olStyle) {
olGeometry = olcsCore.olGeometryCloneTo4326(olGeometry, projection);
console.assert(olGeometry.getType() == 'Circle');
// ol.Coordinate
let center = olGeometry.getCenter();
const height = center.length == 3 ? center[2] : 0.0;
let point = center.slice();
point[0] += olGeometry.getRadius();
// Cesium
center = olcsCore.ol4326CoordinateToCesiumCartesian(center);
point = olcsCore.ol4326CoordinateToCesiumCartesian(point);
// Accurate computation of straight distance
const radius = Cesium.Cartesian3.distance(center, point);
const fillGeometry = new Cesium.CircleGeometry({
// always update Cesium externs before adding a property
center,
radius,
height
});
let outlinePrimitive, outlineGeometry;
if (this.getHeightReference(layer, feature, olGeometry) === Cesium.HeightReference.CLAMP_TO_GROUND) {
const width = this.extractLineWidthFromOlStyle(olStyle);
if (width) {
const circlePolygon = olCreateCircularPolygon(olGeometry.getCenter(), radius);
const positions = olcsCore.ol4326CoordinateArrayToCsCartesians(circlePolygon.getLinearRing(0).getCoordinates());
if (!isGroundPolylinePrimitiveSupported(this.scene)) {
const color = this.extractColorFromOlStyle(olStyle, true);
outlinePrimitive = this.createStackedGroundCorridors(layer, feature, width, color, positions);
} else {
outlinePrimitive = new Cesium.GroundPolylinePrimitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.GroundPolylineGeometry({positions, width}),
}),
appearance: new Cesium.PolylineMaterialAppearance({
material: this.olStyleToCesium(feature, olStyle, true),
}),
classificationType: Cesium.ClassificationType.TERRAIN,
});
outlinePrimitive.readyPromise.then(() => {
this.setReferenceForPicking(layer, feature, outlinePrimitive._primitive);
});
}
}
} else {
outlineGeometry = new Cesium.CircleOutlineGeometry({
// always update Cesium externs before adding a property
center,
radius,
extrudedHeight: height,
height
});
}
const primitives = this.wrapFillAndOutlineGeometries(
layer, feature, olGeometry, fillGeometry, outlineGeometry, olStyle);
if (outlinePrimitive) {
primitives.add(outlinePrimitive);
}
return this.addTextStyle(layer, feature, olGeometry, olStyle, primitives);
}
/**
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!number} width The width of the line.
* @param {!Cesium.Color} color The color of the line.
* @param {!Array<Cesium.Cartesian3>|Array<Array<Cesium.Cartesian3>>} positions The vertices of the line(s).
* @return {!Cesium.GroundPrimitive} primitive
*/
createStackedGroundCorridors(layer, feature, width, color, positions) {
// Convert positions to an Array if it isn't
if (!Array.isArray(positions[0])) {
positions = [positions];
}
width = Math.max(3, width); // A <3px width is too small for ground primitives
const geometryInstances = [];
let previousDistance = 0;
// A stack of ground lines with increasing width (in meters) are created.
// Only one of these lines is displayed at any time giving a feeling of continuity.
// The values for the distance and width factor are more or less arbitrary.
// Applications can override this logics by subclassing the FeatureConverter class.
for (const distance of [1000, 4000, 16000, 64000, 254000, 1000000, 10000000]) {
width *= 2.14;
const geometryOptions = {
// always update Cesium externs before adding a property
width,
vertexFormat: Cesium.VertexFormat.POSITION_ONLY
};
for (const linePositions of positions) {
geometryOptions.positions = linePositions;
geometryInstances.push(new Cesium.GeometryInstance({
geometry: new Cesium.CorridorGeometry(geometryOptions),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(color),
distanceDisplayCondition: new Cesium.DistanceDisplayConditionGeometryInstanceAttribute(previousDistance, distance - 1)
}
}));
}
previousDistance = distance;
}
return new Cesium.GroundPrimitive({
// always update Cesium externs before adding a property
geometryInstances
});
}
/**
* Convert an OpenLayers line string geometry to Cesium.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.LineString} olGeometry OpenLayers line string geometry.
* @param {!ol.ProjectionLike} projection
* @param {!ol.style.Style} olStyle
* @return {!Cesium.PrimitiveCollection} primitives
* @api
*/
olLineStringGeometryToCesium(layer, feature, olGeometry, projection, olStyle) {
olGeometry = olcsCore.olGeometryCloneTo4326(olGeometry, projection);
console.assert(olGeometry.getType() == 'LineString');
const positions = olcsCore.ol4326CoordinateArrayToCsCartesians(olGeometry.getCoordinates());
const width = this.extractLineWidthFromOlStyle(olStyle);
let outlinePrimitive;
const heightReference = this.getHeightReference(layer, feature, olGeometry);
if (heightReference === Cesium.HeightReference.CLAMP_TO_GROUND && !isGroundPolylinePrimitiveSupported(this.scene)) {
const color = this.extractColorFromOlStyle(olStyle, true);
outlinePrimitive = this.createStackedGroundCorridors(layer, feature, width, color, positions);
} else {
const appearance = new Cesium.PolylineMaterialAppearance({
// always update Cesium externs before adding a property
material: this.olStyleToCesium(feature, olStyle, true)
});
const geometryOptions = {
// always update Cesium externs before adding a property
positions,
width,
};
const primitiveOptions = {
// always update Cesium externs before adding a property
appearance
};
if (heightReference === Cesium.HeightReference.CLAMP_TO_GROUND) {
const geometry = new Cesium.GroundPolylineGeometry(geometryOptions);
primitiveOptions.geometryInstances = new Cesium.GeometryInstance({
geometry
}),
outlinePrimitive = new Cesium.GroundPolylinePrimitive(primitiveOptions);
outlinePrimitive.readyPromise.then(() => {
this.setReferenceForPicking(layer, feature, outlinePrimitive._primitive);
});
} else {
geometryOptions.vertexFormat = appearance.vertexFormat;
const geometry = new Cesium.PolylineGeometry(geometryOptions);
primitiveOptions.geometryInstances = new Cesium.GeometryInstance({
geometry
}),
outlinePrimitive = new Cesium.Primitive(primitiveOptions);
}
}
this.setReferenceForPicking(layer, feature, outlinePrimitive);
return this.addTextStyle(layer, feature, olGeometry, olStyle, outlinePrimitive);
}
/**
* Convert an OpenLayers polygon geometry to Cesium.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.Polygon} olGeometry OpenLayers polygon geometry.
* @param {!ol.ProjectionLike} projection
* @param {!ol.style.Style} olStyle
* @return {!Cesium.PrimitiveCollection} primitives
* @api
*/
olPolygonGeometryToCesium(layer, feature, olGeometry, projection, olStyle) {
olGeometry = olcsCore.olGeometryCloneTo4326(olGeometry, projection);
console.assert(olGeometry.getType() == 'Polygon');
const heightReference = this.getHeightReference(layer, feature, olGeometry);
let fillGeometry, outlineGeometry, outlinePrimitive;
if ((olGeometry.getCoordinates()[0].length == 5) &&
(feature.getGeometry().get('olcs.polygon_kind') === 'rectangle')) {
// Create a rectangle according to the longitude and latitude curves
const coordinates = olGeometry.getCoordinates()[0];
// Extract the West, South, East, North coordinates
const extent = boundingExtent(coordinates);
const rectangle = Cesium.Rectangle.fromDegrees(extent[0], extent[1],
extent[2], extent[3]);
// Extract the average height of the vertices
let maxHeight = 0.0;
if (coordinates[0].length == 3) {
for (let c = 0; c < coordinates.length; c++) {
maxHeight = Math.max(maxHeight, coordinates[c][2]);
}
}
// Render the cartographic rectangle
fillGeometry = new Cesium.RectangleGeometry({
ellipsoid: Cesium.Ellipsoid.WGS84,
rectangle,
height: maxHeight
});
outlineGeometry = new Cesium.RectangleOutlineGeometry({
ellipsoid: Cesium.Ellipsoid.WGS84,
rectangle,
height: maxHeight
});
} else {
const rings = olGeometry.getLinearRings();
// always update Cesium externs before adding a property
const hierarchy = {};
const polygonHierarchy = hierarchy;
console.assert(rings.length > 0);
for (let i = 0; i < rings.length; ++i) {
const olPos = rings[i].getCoordinates();
const positions = olcsCore.ol4326CoordinateArrayToCsCartesians(olPos);
console.assert(positions && positions.length > 0);
if (i == 0) {
hierarchy.positions = positions;
} else {
if (!hierarchy.holes) {
hierarchy.holes = [];
}
hierarchy.holes.push({
positions
});
}
}
fillGeometry = new Cesium.PolygonGeometry({
// always update Cesium externs before adding a property
polygonHierarchy,
perPositionHeight: true
});
// Since Cesium doesn't yet support Polygon outlines on terrain yet (coming soon...?)
// we don't create an outline geometry if clamped, but instead do the polyline method
// for each ring. Most of this code should be removeable when Cesium adds
// support for Polygon outlines on terrain.
if (heightReference === Cesium.HeightReference.CLAMP_TO_GROUND) {
const width = this.extractLineWidthFromOlStyle(olStyle);
if (width > 0) {
const positions = [hierarchy.positions];
if (hierarchy.holes) {
for (let i = 0; i < hierarchy.holes.length; ++i) {
positions.push(hierarchy.holes[i].positions);
}
}
if (!isGroundPolylinePrimitiveSupported(this.scene)) {
const color = this.extractColorFromOlStyle(olStyle, true);
outlinePrimitive = this.createStackedGroundCorridors(layer, feature, width, color, positions);
} else {
const appearance = new Cesium.PolylineMaterialAppearance({
// always update Cesium externs before adding a property
material: this.olStyleToCesium(feature, olStyle, true)
});
const geometryInstances = [];
for (const linePositions of positions) {
const polylineGeometry = new Cesium.GroundPolylineGeometry({positions: linePositions, width});
geometryInstances.push(new Cesium.GeometryInstance({
geometry: polylineGeometry
}));
}
const primitiveOptions = {
// always update Cesium externs before adding a property
appearance,
geometryInstances
};
outlinePrimitive = new Cesium.GroundPolylinePrimitive(primitiveOptions);
outlinePrimitive.readyPromise.then(() => {
this.setReferenceForPicking(layer, feature, outlinePrimitive._primitive);
});
}
}
} else {
// Actually do the normal polygon thing. This should end the removable
// section of code described above.
outlineGeometry = new Cesium.PolygonOutlineGeometry({
// always update Cesium externs before adding a property
polygonHierarchy: hierarchy,
perPositionHeight: true
});
}
}
const primitives = this.wrapFillAndOutlineGeometries(
layer, feature, olGeometry, fillGeometry, outlineGeometry, olStyle);
if (outlinePrimitive) {
primitives.add(outlinePrimitive);
}
return this.addTextStyle(layer, feature, olGeometry, olStyle, primitives);
}
/**
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.Geometry} geometry
* @return {!Cesium.HeightReference}
* @api
*/
getHeightReference(layer, feature, geometry) {
// Read from the geometry
let altitudeMode = geometry.get('altitudeMode');
// Or from the feature
if (altitudeMode === undefined) {
altitudeMode = feature.get('altitudeMode');
}
// Or from the layer
if (altitudeMode === undefined) {
altitudeMode = layer.get('altitudeMode');
}
let heightReference = Cesium.HeightReference.NONE;
if (altitudeMode === 'clampToGround') {
heightReference = Cesium.HeightReference.CLAMP_TO_GROUND;
} else if (altitudeMode === 'relativeToGround') {
heightReference = Cesium.HeightReference.RELATIVE_TO_GROUND;
}
return heightReference;
}
/**
* Convert a point geometry to a Cesium BillboardCollection.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.Point} olGeometry OpenLayers point geometry.
* @param {!ol.ProjectionLike} projection
* @param {!ol.style.Style} style
* @param {!ol.style.Image} imageStyle
* @param {!Cesium.BillboardCollection} billboards
* @param {function(!Cesium.Billboard)=} opt_newBillboardCallback Called when the new billboard is added.
* @api
*/
createBillboardFromImage(
layer,
feature,
olGeometry,
projection,
style,
imageStyle,
billboards,
opt_newBillboardCallback
) {
if (imageStyle instanceof olStyleIcon) {
// make sure the image is scheduled for load
imageStyle.load();
}
const image = imageStyle.getImage(1); // get normal density
const isImageLoaded = function(image) {
return image.src != '' &&
image.naturalHeight != 0 &&
image.naturalWidth != 0 &&
image.complete;
};
const reallyCreateBillboard = (function() {
if (!image) {
return;
}
if (!(image instanceof HTMLCanvasElement ||
image instanceof Image ||
image instanceof HTMLImageElement)) {
return;
}
const center = olGeometry.getCoordinates();
const position = olcsCore.ol4326CoordinateToCesiumCartesian(center);
let color;
const opacity = imageStyle.getOpacity();
if (opacity !== undefined) {
color = new Cesium.Color(1.0, 1.0, 1.0, opacity);
}
const heightReference = this.getHeightReference(layer, feature, olGeometry);
const bbOptions = /** @type {Cesium.optionsBillboardCollectionAdd} */ ({
// always update Cesium externs before adding a property
image,
color,
scale: imageStyle.getScale(),
heightReference,
position
});
if (imageStyle instanceof olStyleIcon) {
const anchor = imageStyle.getAnchor();
if (anchor) {
bbOptions.pixelOffset = new Cesium.Cartesian2(image.width / 2 - anchor[0], image.height / 2 - anchor[1]);
}
}
const bb = this.csAddBillboard(billboards, bbOptions, layer, feature, olGeometry, style);
if (opt_newBillboardCallback) {
opt_newBillboardCallback(bb);
}
}).bind(this);
if (image instanceof Image && !isImageLoaded(image)) {
// Cesium requires the image to be loaded
let cancelled = false;
const source = layer.getSource();
const canceller = function() {
cancelled = true;
};
source.on(['removefeature', 'clear'],
this.boundOnRemoveOrClearFeatureListener_);
let cancellers = olcsUtil.obj(source)['olcs_cancellers'];
if (!cancellers) {
cancellers = olcsUtil.obj(source)['olcs_cancellers'] = {};
}
const fuid = getUid(feature);
if (cancellers[fuid]) {
// When the feature change quickly, a canceller may still be present so
// we cancel it here to prevent creation of a billboard.
cancellers[fuid]();
}
cancellers[fuid] = canceller;
const listener = function() {
image.removeEventListener('load', listener);
if (!billboards.isDestroyed() && !cancelled) {
// Create billboard if the feature is still displayed on the map.
reallyCreateBillboard();
}
};
image.addEventListener('load', listener);
} else {
reallyCreateBillboard();
}
}
/**
* Convert a point geometry to a Cesium BillboardCollection.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.Point} olGeometry OpenLayers point geometry.
* @param {!ol.ProjectionLike} projection
* @param {!ol.style.Style} style
* @param {!Cesium.BillboardCollection} billboards
* @param {function(!Cesium.Billboard)=} opt_newBillboardCallback Called when
* the new billboard is added.
* @return {Cesium.Primitive} primitives
* @api
*/
olPointGeometryToCesium(
layer,
feature,
olGeometry,
projection,
style,
billboards,
opt_newBillboardCallback
) {
console.assert(olGeometry.getType() == 'Point');
olGeometry = olcsCore.olGeometryCloneTo4326(olGeometry, projection);
let modelPrimitive = null;
const imageStyle = style.getImage();
if (imageStyle) {
const olcsModelFunction = /** @type {function():olcsx.ModelStyle} */ (olGeometry.get('olcs_model') || feature.get('olcs_model'));
if (olcsModelFunction) {
const olcsModel = olcsModelFunction();
const options = /** @type {Cesium.ModelFromGltfOptions} */ (Object.assign({}, {scene: this.scene}, olcsModel.cesiumOptions));
const model = Cesium.Model.fromGltf(options);
modelPrimitive = new Cesium.PrimitiveCollection();
modelPrimitive.add(model);
if (olcsModel.debugModelMatrix) {
modelPrimitive.add(new Cesium.DebugModelMatrixPrimitive({
modelMatrix: olcsModel.debugModelMatrix
}));
}
} else {
this.createBillboardFromImage(layer, feature, olGeometry, projection, style, imageStyle, billboards, opt_newBillboardCallback);
}
}
if (style.getText()) {
return this.addTextStyle(layer, feature, olGeometry, style, modelPrimitive || new Cesium.Primitive());
} else {
return modelPrimitive;
}
}
/**
* Convert an OpenLayers multi-something geometry to Cesium.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.Geometry} geometry OpenLayers geometry.
* @param {!ol.ProjectionLike} projection
* @param {!ol.style.Style} olStyle
* @param {!Cesium.BillboardCollection} billboards
* @param {function(!Cesium.Billboard)=} opt_newBillboardCallback Called when
* the new billboard is added.
* @return {Cesium.Primitive} primitives
* @api
*/
olMultiGeometryToCesium(
layer,
feature,
geometry,
projection,
olStyle,
billboards,
opt_newBillboardCallback
) {
// Do not reproject to 4326 now because it will be done later.
// FIXME: would be better to combine all child geometries in one primitive
// instead we create n primitives for simplicity.
const accumulate = function(geometries, functor) {
const primitives = new Cesium.PrimitiveCollection();
geometries.forEach((geometry) => {
primitives.add(functor(layer, feature, geometry, projection, olStyle));
});
return primitives;
};
let subgeos;
switch (geometry.getType()) {
case 'MultiPoint':
geometry = /** @type {!ol.geom.MultiPoint} */ (geometry);
subgeos = geometry.getPoints();
if (olStyle.getText()) {
const primitives = new Cesium.PrimitiveCollection();
subgeos.forEach((geometry) => {
console.assert(geometry);
const result = this.olPointGeometryToCesium(layer, feature, geometry,
projection, olStyle, billboards, opt_newBillboardCallback);
if (result) {
primitives.add(result);
}
});
return primitives;
} else {
subgeos.forEach((geometry) => {
console.assert(geometry);
this.olPointGeometryToCesium(layer, feature, geometry, projection,
olStyle, billboards, opt_newBillboardCallback);
});
return null;
}
case 'MultiLineString':
geometry = /** @type {!ol.geom.MultiLineString} */ (geometry);
subgeos = geometry.getLineStrings();
return accumulate(subgeos, this.olLineStringGeometryToCesium.bind(this));
case 'MultiPolygon':
geometry = /** @type {!ol.geom.MultiPolygon} */ (geometry);
subgeos = geometry.getPolygons();
return accumulate(subgeos, this.olPolygonGeometryToCesium.bind(this));
default:
console.assert(false, `Unhandled multi geometry type${geometry.getType()}`);
}
}
/**
* Convert an OpenLayers text style to Cesium.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature..
* @param {!ol.geom.Geometry} geometry
* @param {!ol.style.Text} style
* @return {Cesium.LabelCollection} Cesium primitive
* @api
*/
olGeometry4326TextPartToCesium(layer, feature, geometry, style) {
const text = style.getText();
if (!text) {
return null;
}
const labels = new Cesium.LabelCollection({scene: this.scene});
// TODO: export and use the text draw position from OpenLayers .
// See src/ol/render/vector.js
const extentCenter = getCenter(geometry.getExtent());
if (geometry instanceof olGeomSimpleGeometry) {
const first = geometry.getFirstCoordinate();
extentCenter[2] = first.length == 3 ? first[2] : 0.0;
}
const options = /** @type {Cesium.optionsLabelCollection} */ ({});
options.position = olcsCore.ol4326CoordinateToCesiumCartesian(extentCenter);
options.text = text;
options.heightReference = this.getHeightReference(layer, feature, geometry);
const offsetX = style.getOffsetX();
const offsetY = style.getOffsetY();
if (offsetX != 0 && offsetY != 0) {
const offset = new Cesium.Cartesian2(offsetX, offsetY);
options.pixelOffset = offset;
}
options.font = style.getFont() || '10px sans-serif'; // OpenLayers default
let labelStyle = undefined;
if (style.getFill()) {
options.fillColor = this.extractColorFromOlStyle(style, false);
labelStyle = Cesium.LabelStyle.FILL;
}
if (style.getStroke()) {
options.outlineWidth = this.extractLineWidthFromOlStyle(style);
options.outlineColor = this.extractColorFromOlStyle(style, true);
labelStyle = Cesium.LabelStyle.OUTLINE;
}
if (style.getFill() && style.getStroke()) {
labelStyle = Cesium.LabelStyle.FILL_AND_OUTLINE;
}
options.style = labelStyle;
let horizontalOrigin;
switch (style.getTextAlign()) {
case 'left':
horizontalOrigin = Cesium.HorizontalOrigin.LEFT;
break;
case 'right':
horizontalOrigin = Cesium.HorizontalOrigin.RIGHT;
break;
case 'center':
default:
horizontalOrigin = Cesium.HorizontalOrigin.CENTER;
}
options.horizontalOrigin = horizontalOrigin;
if (style.getTextBaseline()) {
let verticalOrigin;
switch (style.getTextBaseline()) {
case 'top':
verticalOrigin = Cesium.VerticalOrigin.TOP;
break;
case 'middle':
verticalOrigin = Cesium.VerticalOrigin.CENTER;
break;
case 'bottom':
verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
break;
case 'alphabetic':
verticalOrigin = Cesium.VerticalOrigin.TOP;
break;
case 'hanging':
verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
break;
default:
console.assert(false, `unhandled baseline ${style.getTextBaseline()}`);
}
options.verticalOrigin = verticalOrigin;
}
const l = labels.add(options);
this.setReferenceForPicking(layer, feature, l);
return labels;
}
/**
* Convert an OpenLayers style to a Cesium Material.
* @param {ol.Feature} feature OpenLayers feature..
* @param {!ol.style.Style} style
* @param {boolean} outline
* @return {Cesium.Material}
* @api
*/
olStyleToCesium(feature, style, outline) {
const fill = style.getFill();
const stroke = style.getStroke();
if ((outline && !stroke) || (!outline && !fill)) {
return null; // FIXME use a default style? Developer error?
}
let color = outline ? stroke.getColor() : fill.getColor();
color = olcsCore.convertColorToCesium(color);
if (outline && stroke.getLineDash()) {
return Cesium.Material.fromType('Stripe', {
// always update Cesium externs before adding a property
horizontal: false,
repeat: 500, // TODO how to calculate this?
evenColor: color,
oddColor: new Cesium.Color(0, 0, 0, 0) // transparent
});
} else {
return Cesium.Material.fromType('Color', {
// always update Cesium externs before adding a property
color
});
}
}
/**
* Compute OpenLayers plain style.
* Evaluates style function, blend arrays, get default style.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature
* @param {ol.StyleFunction|undefined} fallbackStyleFunction
* @param {number} resolution
* @return {Array.<!ol.style.Style>} null if no style is available
* @api
*/
computePlainStyle(layer, feature, fallbackStyleFunction, resolution) {
/**
* @type {ol.FeatureStyleFunction|undefined}
*/
const featureStyleFunction = feature.getStyleFunction();
/**
* @type {ol.style.Style|Array.<ol.style.Style>}
*/
let style = null;
if (featureStyleFunction) {
style = featureStyleFunction(feature, resolution);
}
if (!style && fallbackStyleFunction) {
style = fallbackStyleFunction(feature, resolution);
}
if (!style) {
// The feature must not be displayed
return null;
}
// FIXME combine materials as in cesium-materials-pack?
// then this function must return a custom material
// More simply, could blend the colors like described in
// http://en.wikipedia.org/wiki/Alpha_compositing
return Array.isArray(style) ? style : [style];
}
/**
* @protected
* @param {!ol.Feature} feature
* @param {!ol.style.Style} style
* @param {!ol.geom.Geometry=} opt_geom Geometry to be converted.
* @return {ol.geom.Geometry|undefined}
*/
getGeometryFromFeature(feature, style, opt_geom) {
if (opt_geom) {
return opt_geom;
}
const geom3d = /** @type {!ol.geom.Geometry} */(feature.get('olcs.3d_geometry'));
if (geom3d && geom3d instanceof olGeomGeometry) {
return geom3d;
}
if (style) {
const geomFuncRes = style.getGeometryFunction()(feature);
if (geomFuncRes instanceof olGeomGeometry) {
return geomFuncRes;
}
}
return feature.getGeometry();
}
/**
* Convert one OpenLayers feature up to a collection of Cesium primitives.
* @param {ol.layer.Vector|ol.layer.Image} layer
* @param {!ol.Feature} feature OpenLayers feature.
* @param {!ol.style.Style} style
* @param {!import('olcs/core/VectorLayerConterpart.js').OlFeatureToCesiumContext} context
* @param {!ol.geom.Geometry=} opt_geom Geometry to be converted.
* @return {Cesium.Primitive} primitives
* @api
*/
olFeatureToCesium(layer, feature, style, context, opt_geom) {
let geom = this.getGeometryFromFeature(feature, style, opt_geom);
if (!geom) {
// OpenLayers features may not have a geometry
// See http://geojson.org/geojson-spec.html#feature-objects
return null;
}
const proj = context.projection;
const newBillboardAddedCallback = function(bb) {
const featureBb = context.featureToCesiumMap[getUid(feature)];
if (featureBb instanceof Array) {
featureBb.push(bb);
}
else {
context.featureToCesiumMap[getUid(feature)] = [bb];
}
};
switch (geom.getType()) {
case 'GeometryCollection':
const primitives = new Cesium.PrimitiveCollection();
const collection = /** @type {!ol.geom.GeometryCollection} */ (geom);
// TODO: use getGeometriesArray() instead
collection.getGeometries().forEach((geom) => {
if (geom) {
const prims = this.olFeatureToCesium(layer, feature, style, context,
geom);
if (prims) {
primitives.add(prims);
}
}
});
return primitives;
case 'Point':
geom = /** @type {!ol.geom.Point} */ (geom);
const bbs = context.billboards;
const result = this.olPointGeometryToCesium(layer, feature, geom, proj,
style, bbs, newBillboardAddedCallback);
if (!result) {
// no wrapping primitive
return null;
} else {
return result;
}
case 'Circle':
geom = /** @type {!ol.geom.Circle} */ (geom);
return this.olCircleGeometryToCesium(layer, feature, geom, proj,
style);
case 'LineString':
geom = /** @type {!ol.geom.LineString} */ (geom);
return this.olLineStringGeometryToCesium(layer, feature, geom, proj,
style);
case 'Polygon':
geom = /** @type {!ol.geom.Polygon} */ (geom);
return this.olPolygonGeometryToCesium(layer, feature, geom, proj,
style);
case 'MultiPoint':
case 'MultiLineString':
case 'MultiPolygon':
const result2 = this.olMultiGeometryToCesium(layer, feature, geom, proj,
style, context.billboards, newBillboardAddedCallback);
if (!result2) {
// no wrapping primitive
return null;
} else {
return result2;
}
case 'LinearRing':
throw new Error('LinearRing should only be part of polygon.');
default:
throw new Error(`Ol geom type not handled : ${geom.getType()}`);
}
}
/**
* Convert an OpenLayers vector layer to Cesium primitive collection.
* For each feature, the associated primitive will be stored in
* `featurePrimitiveMap`.
* @param {!(ol.layer.Vector|ol.layer.Image)} olLayer
* @param {!ol.View} olView
* @param {!Object.<number, !Cesium.Primitive>} featurePrimitiveMap
* @return {!olcs.core.VectorLayerCounterpart}
* @api
*/
olVectorLayerToCesium(olLayer, olView, featurePrimitiveMap) {
const proj = olView.getProjection();
const resolution = olView.getResolution();
if (resolution === undefined || !proj) {
console.assert(false, 'View not ready');
// an assertion is not enough for closure to assume resolution and proj
// are defined
throw new Error('View not ready');
}
let source = olLayer.getSource();
if (source instanceof olSourceCluster) {
source = source.getSource();
}
console.assert(source instanceof olSourceVector);
const features = source.getFeatures();
const counterpart = new olcsCoreVectorLayerCounterpart(proj, this.scene);
const context = counterpart.context;
for (let i = 0; i < features.length; ++i) {
const feature = features[i];
if (!feature) {
continue;
}
/**
* @type {ol.StyleFunction|undefined}
*/
const layerStyle = olLayer.getStyleFunction();
const styles = this.computePlainStyle(olLayer, feature, layerStyle,
resolution);
if (!styles || !styles.length) {
// only 'render' features with a style
continue;
}
/**
* @type {Cesium.Primitive|null}
*/
let primitives = null;
for (let i = 0; i < styles.length; i++) {
const prims = this.olFeatureToCesium(olLayer, feature, styles[i], context);
if (prims) {
if (!primitives) {
primitives = prims;
} else if (prims) {
let i = 0, prim;
while ((prim = prims.get(i))) {
primitives.add(prim);
i++;
}
}
}
}
if (!primitives) {
continue;
}
featurePrimitiveMap[getUid(feature)] = primitives;
counterpart.getRootPrimitive().add(primitives);
}
return counterpart;
}
/**
* Convert an OpenLayers feature to Cesium primitive collection.
* @param {!(ol.layer.Vector|ol.layer.Image)} layer
* @param {!ol.View} view
* @param {!ol.Feature} feature
* @param {!import('olcs/core/VectorLayerConterpart.js').OlFeatureToCesiumContext} context
* @return {Cesium.Primitive}
* @api
*/
convert(layer, view, feature, context) {
const proj = view.getProjection();
const resolution = view.getResolution();
if (resolution == undefined || !proj) {
return null;
}
/**
* @type {ol.StyleFunction|undefined}
*/
const layerStyle = layer.getStyleFunction();
const styles = this.computePlainStyle(layer, feature, layerStyle, resolution);
if (!styles.length) {
// only 'render' features with a style
return null;
}
context.projection = proj;
/**
* @type {Cesium.Primitive|null}
*/
let primitives = null;
for (let i = 0; i < styles.length; i++) {
const prims = this.olFeatureToCesium(layer, feature, styles[i], context);
if (!primitives) {
primitives = prims;
} else if (prims) {
let i = 0, prim;
while ((prim = prims.get(i))) {
primitives.add(prim);
i++;
}
}
}
return primitives;
}
}
export default FeatureConverter;