ev-olcs
Version:
OpenLayers Cesium integration and plugin library
1,090 lines • 89.9 kB
JavaScript
import OLStyleIcon from 'ol/style/Icon.js';
import VectorSource, {} from 'ol/source/Vector.js';
import OLClusterSource 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 { convertColorToCesium, olGeometryCloneTo4326, ol4326CoordinateToCesiumCartesian, ol4326CoordinateArrayToCsCartesians } from './core.js';
import VectorLayerCounterpart, {} from './core/VectorLayerCounterpart.js';
import { getUid, waitReady } from './util.js';
import {} from 'cesium';
import {} from 'ol/style/Style.js';
import { Geometry as OLGeometry } from 'ol/geom.js';
import { clone } from 'cesium';
export default class FeatureConverter {
scene;
/**
* Bind once to have a unique function for using as a listener
*/
boundOnRemoveOrClearFeatureListener_ = this.onRemoveOrClearFeature_.bind(this);
defaultBillboardEyeOffset_ = new Cesium.Cartesian3(0, 0, 10);
/**
* 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 scene Cesium scene.
* @api
*/
constructor(scene) {
this.scene = scene;
this.scene = scene;
}
/**
* @param evt
*/
onRemoveOrClearFeature_(evt) {
const source = evt.target;
console.assert(source instanceof VectorSource);
const cancellers = 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]();
}
}
source['olcs_cancellers'] = {};
}
}
}
/**
* @param layer
* @param feature OpenLayers feature.
* @param primitive
*/
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 layer
* @param feature OpenLayers feature.
* @param olGeometry OpenLayers geometry.
* @param geometry
* @param color
* @param opt_lineWidth
* @return primitive
*/
createColoredPrimitive(layer, feature, olGeometry, geometry, color, opt_lineWidth) {
const createInstance = function (geometry, color) {
const prop = clone(feature.getProperties());
Reflect.deleteProperty(prop, "geometry");
const instance = new Cesium.GeometryInstance({
geometry,
id: prop // Set id here on GeometryInstance
});
if (color && !(color instanceof Cesium.ImageMaterialProperty)) {
instance.attributes = {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(color)
};
}
return instance;
};
const options = {
flat: true, // work with all geometries
renderState: {
depthTest: {
enabled: true
}
}
};
if (opt_lineWidth !== undefined) {
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) {
if (!('createShadowVolume' in instances.geometry.constructor)) {
// This is not a ground geometry
return null;
}
primitive = new Cesium.GroundPrimitive({
geometryInstances: instances
});
}
else {
primitive = new Cesium.Primitive({
geometryInstances: instances
});
}
if (color instanceof Cesium.ImageMaterialProperty) {
// FIXME: we created stylings which are not time related
// What should we pass here?
// @ts-ignore
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.MaterialAppearance({
...options,
material: new Cesium.Material({
translucent: color.alpha !== 1,
fabric: {
type: 'Color',
uniforms: {
color,
}
}
})
});
if (primitive instanceof Cesium.Primitive && (feature.get('olcs_shadows') || layer.get('olcs_shadows'))) {
primitive.shadows = 1;
}
}
this.setReferenceForPicking(layer, feature, primitive);
return primitive;
}
/**
* Return the fill or stroke color from a plain ol style.
*/
extractColorFromOlStyle(style, outline) {
const fillColor = style.getFill()?.getColor();
const strokeColor = style.getStroke() ? style.getStroke().getColor() : null;
let olColor = 'black';
if (strokeColor && outline) {
olColor = strokeColor;
}
else if (fillColor) {
olColor = fillColor;
}
const csColor = convertColorToCesium(olColor);
if (csColor instanceof Cesium.ImageMaterialProperty) {
return csColor;
}
if ('red' in csColor) {
return csColor;
}
else {
// Fallback to black if that was not a plain color
return Cesium.Color.BLACK;
}
}
/**
* Return the width of stroke from a plain ol style.
* @param style
* @return {number}
*/
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.
*/
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
// FIXME: would make more sense to only accept primitive collection.
/**
* Create a Cesium primitive if style has a text component.
* Eventually return a PrimitiveCollection including current primitive.
*/
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 billboards
* @param bbOptions
* @param layer
* @param feature OpenLayers feature.
* @param geometry
* @param style
* @return 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.
* @api
*/
olCircleGeometryToCesium(layer, feature, olGeometry, projection, olStyle) {
olGeometry = olGeometryCloneTo4326(olGeometry, projection);
console.assert(olGeometry.getType() == 'Circle');
// ol.Coordinate
const olCenter = olGeometry.getCenter();
const height = olCenter.length == 3 ? olCenter[2] : 0.0;
const olPoint = olCenter.slice();
olPoint[0] += olGeometry.getRadius();
// Cesium
const center = ol4326CoordinateToCesiumCartesian(olCenter);
const point = ol4326CoordinateToCesiumCartesian(olPoint);
// Accurate computation of straight distance
const radius = Cesium.Cartesian3.distance(center, point);
const prop = clone(feature.getProperties());
Reflect.deleteProperty(prop, "geometry");
const fillGeometry = new Cesium.CircleGeometry({
center,
radius,
height
});
let outlinePrimitive;
let 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 = ol4326CoordinateArrayToCsCartesians(circlePolygon.getLinearRing(0).getCoordinates());
const op = outlinePrimitive = new Cesium.GroundPolylinePrimitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.GroundPolylineGeometry({ positions, width }),
id: prop // Set id for GroundPolyline
}),
appearance: new Cesium.PolylineMaterialAppearance({
material: this.olStyleToCesium(feature, olStyle, true),
}),
classificationType: Cesium.ClassificationType.TERRAIN,
});
waitReady(outlinePrimitive).then(() => {
this.setReferenceForPicking(layer, feature, op._primitive);
});
}
}
else {
outlineGeometry = new Cesium.CircleOutlineGeometry({
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);
}
/**
* Convert an OpenLayers line string geometry to Cesium.
* @api
*/
olLineStringGeometryToCesium(layer, feature, olGeometry, projection, olStyle) {
olGeometry = olGeometryCloneTo4326(olGeometry, projection);
console.assert(olGeometry.getType() == 'LineString');
const positions = ol4326CoordinateArrayToCsCartesians(olGeometry.getCoordinates());
const width = this.extractLineWidthFromOlStyle(olStyle);
const prop = clone(feature.getProperties());
Reflect.deleteProperty(prop, "geometry");
let outlinePrimitive;
const heightReference = this.getHeightReference(layer, feature, olGeometry);
const appearance = new Cesium.PolylineMaterialAppearance({
material: this.olStyleToCesium(feature, olStyle, true)
});
if (heightReference === Cesium.HeightReference.CLAMP_TO_GROUND) {
const geometry = new Cesium.GroundPolylineGeometry({
positions,
width,
});
const op = outlinePrimitive = new Cesium.GroundPolylinePrimitive({
appearance,
geometryInstances: new Cesium.GeometryInstance({
geometry,
id: prop // Set id here for GroundPolyline
})
});
waitReady(outlinePrimitive).then(() => {
this.setReferenceForPicking(layer, feature, op._primitive);
});
}
else {
const geometry = new Cesium.PolylineGeometry({
positions,
width,
vertexFormat: appearance.vertexFormat
});
outlinePrimitive = new Cesium.Primitive({
appearance,
geometryInstances: new Cesium.GeometryInstance({
geometry,
id: prop // Set id here for normal Polyline
}),
});
}
this.setReferenceForPicking(layer, feature, outlinePrimitive);
return this.addTextStyle(layer, feature, olGeometry, olStyle, outlinePrimitive);
}
/**
* Convert an OpenLayers polygon geometry to Cesium.
* @api
*/
olPolygonGeometryToCesium(layer, feature, olGeometry, projection, olStyle) {
olGeometry = olGeometryCloneTo4326(olGeometry, projection);
console.assert(olGeometry.getType() == 'Polygon');
const heightReference = this.getHeightReference(layer, feature, olGeometry);
let fillGeometry, outlineGeometry;
let outlinePrimitive;
const prop = clone(feature.getProperties());
Reflect.deleteProperty(prop, "geometry");
if ((olGeometry.getCoordinates()[0].length == 5) &&
(feature.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]);
}
}
const featureExtrudedHeight = feature.get('olcs_extruded_height');
// Render the cartographic rectangle
fillGeometry = new Cesium.RectangleGeometry({
ellipsoid: Cesium.Ellipsoid.WGS84,
rectangle,
height: maxHeight,
extrudedHeight: featureExtrudedHeight,
});
outlineGeometry = new Cesium.RectangleOutlineGeometry({
ellipsoid: Cesium.Ellipsoid.WGS84,
rectangle,
height: maxHeight,
extrudedHeight: featureExtrudedHeight,
});
}
else {
const rings = olGeometry.getLinearRings();
const hierarchy = {
positions: [],
holes: [],
};
const polygonHierarchy = hierarchy;
console.assert(rings.length > 0);
for (let i = 0; i < rings.length; ++i) {
const olPos = rings[i].getCoordinates();
const positions = ol4326CoordinateArrayToCsCartesians(olPos);
console.assert(positions && positions.length > 0);
if (i === 0) {
hierarchy.positions = positions;
}
else {
hierarchy.holes.push({
positions,
holes: [],
});
}
}
const featureExtrudedHeight = feature.get('olcs_extruded_height');
fillGeometry = new Cesium.PolygonGeometry({
polygonHierarchy,
perPositionHeight: true,
extrudedHeight: featureExtrudedHeight,
});
// 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);
}
}
const appearance = new Cesium.PolylineMaterialAppearance({
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,
id: prop // Set id for outline
}));
}
outlinePrimitive = new Cesium.GroundPolylinePrimitive({
appearance,
geometryInstances
});
waitReady(outlinePrimitive).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({
polygonHierarchy: hierarchy,
perPositionHeight: true,
extrudedHeight: featureExtrudedHeight,
});
}
}
const primitives = this.wrapFillAndOutlineGeometries(layer, feature, olGeometry, fillGeometry, outlineGeometry, olStyle);
if (outlinePrimitive) {
primitives.add(outlinePrimitive);
}
return this.addTextStyle(layer, feature, olGeometry, olStyle, primitives);
}
/**
* @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 = ol4326CoordinateToCesiumCartesian(center);
let color;
const opacity = imageStyle.getOpacity();
if (opacity !== undefined) {
color = new Cesium.Color(1.0, 1.0, 1.0, opacity);
}
const scale = imageStyle.getScale();
const heightReference = this.getHeightReference(layer, feature, olGeometry);
const prop = clone(feature.getProperties());
Reflect.deleteProperty(prop, "geometry"); // remove geometry from getProperties
const bbOptions = {
image: image,
color,
scale: Array.isArray(scale) ? (scale[0] + scale[1]) / 2 : scale,
heightReference,
position,
id: prop
};
// merge in cesium options from openlayers feature
Object.assign(bbOptions, feature.get('cesiumOptions'));
if (imageStyle instanceof OLStyleIcon) {
const anchor = imageStyle.getAnchor();
if (anchor) {
const xScale = (Array.isArray(scale) ? scale[0] : scale);
const yScale = (Array.isArray(scale) ? scale[1] : scale);
bbOptions.pixelOffset = new Cesium.Cartesian2((image.width / 2 - anchor[0]) * xScale, (image.height / 2 - anchor[1]) * yScale);
}
}
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 = source['olcs_cancellers'];
if (!cancellers) {
cancellers = 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 layer
* @param feature OpenLayers feature..
* @param olGeometry OpenLayers point geometry.
* @param projection
* @param style
* @param billboards
* @param opt_newBillboardCallback Called when the new billboard is added.
* @return primitives
* @api
*/
olPointGeometryToCesium(layer, feature, olGeometry, projection, style, billboards, opt_newBillboardCallback) {
console.assert(olGeometry.getType() == 'Point');
olGeometry = olGeometryCloneTo4326(olGeometry, projection);
let modelPrimitive = null;
const imageStyle = style.getImage();
if (imageStyle) {
const olcsModelFunction = olGeometry.get('olcs_model') || feature.get('olcs_model');
if (olcsModelFunction) {
modelPrimitive = new Cesium.PrimitiveCollection();
const olcsModel = olcsModelFunction();
const options = Object.assign({}, { scene: this.scene }, olcsModel.cesiumOptions);
if ('fromGltf' in Cesium.Model) {
// pre Cesium v107
// @ts-ignore
const model = Cesium.Model.fromGltf(options);
modelPrimitive.add(model);
}
else {
Cesium.Model.fromGltfAsync(options).then((model) => {
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.
switch (geometry.getType()) {
case 'MultiPoint': {
const points = geometry.getPoints();
if (olStyle.getText()) {
const primitives = new Cesium.PrimitiveCollection();
points.forEach((geom) => {
console.assert(geom);
const result = this.olPointGeometryToCesium(layer, feature, geom, projection, olStyle, billboards, opt_newBillboardCallback);
if (result) {
primitives.add(result);
}
});
return primitives;
}
else {
points.forEach((geom) => {
console.assert(geom);
this.olPointGeometryToCesium(layer, feature, geom, projection, olStyle, billboards, opt_newBillboardCallback);
});
return null;
}
}
case 'MultiLineString': {
const lineStrings = geometry.getLineStrings();
// FIXME: would be better to combine all child geometries in one primitive
// instead we create n primitives for simplicity.
const primitives = new Cesium.PrimitiveCollection();
lineStrings.forEach((geom) => {
const p = this.olLineStringGeometryToCesium(layer, feature, geom, projection, olStyle);
primitives.add(p);
});
return primitives;
}
case 'MultiPolygon': {
const polygons = geometry.getPolygons();
// FIXME: would be better to combine all child geometries in one primitive
// instead we create n primitives for simplicity.
const primitives = new Cesium.PrimitiveCollection();
polygons.forEach((geom) => {
const p = this.olPolygonGeometryToCesium(layer, feature, geom, projection, olStyle);
primitives.add(p);
});
return primitives;
}
default:
console.assert(false, `Unhandled multi geometry type${geometry.getType()}`);
}
}
/**
* Convert an OpenLayers text style to Cesium.
* @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 = {
position: ol4326CoordinateToCesiumCartesian(extentCenter)
};
options.text = Array.isArray(text) ? text.join(' ') : 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.
* @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?
}
const olColor = outline ? stroke.getColor() : fill.getColor();
const color = convertColorToCesium(olColor);
const lineDash = stroke.getLineDash();
if (outline && lineDash) {
return Cesium.Material.fromType('PolylineDash', {
dashPattern: dashPattern(lineDash),
color
});
}
else {
return Cesium.Material.fromType('Color', {
color
});
}
}
/**
* Compute OpenLayers plain style.
* Evaluates style function, blend arrays, get default style.
* @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];
}
/**
*/
getGeometryFromFeature(feature, style, opt_geom) {
if (opt_geom) {
return opt_geom;
}
const geom3d = feature.get('olcs_3d_geometry');
if (geom3d && geom3d instanceof OLGeometry) {
return geom3d;
}
if (style) {
const geomFuncRes = style.getGeometryFunction()(feature);
if (geomFuncRes instanceof OLGeometry) {
return geomFuncRes;
}
}
return feature.getGeometry();
}
/**
* Convert one OpenLayers feature up to a collection of Cesium primitives.
* @api
*/
olFeatureToCesium(layer, feature, style, context, opt_geom) {
const 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];
}
};
let primitives;
switch (geom.getType()) {
case 'GeometryCollection':
primitives = new Cesium.PrimitiveCollection();
geom.getGeometriesArray().forEach((geom) => {
if (geom) {
const prims = this.olFeatureToCesium(layer, feature, style, context, geom);
if (prims) {
primitives.add(prims);
}
}
});
break;
case 'Point':
const bbs = context.billboards;
primitives = this.olPointGeometryToCesium(layer, feature, geom, proj, style, bbs, newBillboardAddedCallback);
break;
case 'Circle':
primitives = this.olCircleGeometryToCesium(layer, feature, geom, proj, style);
break;
case 'LineString':
primitives = this.olLineStringGeometryToCesium(layer, feature, geom, proj, style);
break;
case 'Polygon':
primitives = this.olPolygonGeometryToCesium(layer, feature, geom, proj, style);
break;
case 'MultiPoint':
primitives = this.olMultiGeometryToCesium(layer, feature, geom, proj, style, context.billboards, newBillboardAddedCallback) || null;
break;
case 'MultiLineString':
primitives = this.olMultiGeometryToCesium(layer, feature, geom, proj, style, context.billboards, newBillboardAddedCallback) || null;
break;
case 'MultiPolygon':
primitives = this.olMultiGeometryToCesium(layer, feature, geom, proj, style, context.billboards, newBillboardAddedCallback) || null;
break;
case 'LinearRing':
throw new Error('LinearRing should only be part of polygon.');
default:
throw new Error(`Ol geom type not handled : ${geom.getType()}`);
}
return primitives;
}
/**
* Convert an OpenLayers vector layer to Cesium primitive collection.
* For each feature, the associated primitive will be stored in
* `featurePrimitiveMap`.
* @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 OLClusterSource) {
source = source.getSource();
}
console.assert(source instanceof VectorSource);
const features = source.getFeatures();
const counterpart = new VectorLayerCounterpart(proj, this.scene);
const context = counterpart.context;
for (let i = 0; i < features.length; ++i) {
const feature = features[i];
if (!feature) {
continue;
}
const layerStyle = olLayer.getStyleFunction();
const styles = this.computePlainStyle(olLayer, feature, layerStyle, resolution);
if (!styles || !styles.length) {
// only 'render' features with a style
continue;
}
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.
* @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 || !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;
}
}
/**
* Transform a canvas line dash pattern to a Cesium dash pattern
* See https://cesium.com/learn/cesiumjs/ref-doc/PolylineDashMaterialProperty.html#dashPattern
* @param lineDash
*/
export function dashPattern(lineDash) {
if (lineDash.length < 2) {
lineDash = [1, 1];
}
const segments = lineDash.length % 2 === 0 ? lineDash : [...lineDash, ...lineDash];
const total = segments.reduce((a, b) => a + b, 0);
const div = total / 16;
// create a 16 bit binary string
let binaryString = segments.map((segment, index) => {
// we alternate between 1 and 0
const digit = index % 2 === 0 ? '1' : '0';
// We scale the segment length to fit 16 slots.
let count = Math.round(segment / div);
if (index === 0 && count === 0) {
// We need to start with a 1
count = 1;
}
return digit.repeat(count);
}).join('');
// We rounded so it might be that the string is too short or too long.
// We try to fix it by padding or truncating the string.
if (binaryString.length < 16) {
binaryString = binaryString.padEnd(16, '0');
}
else if (binaryString.length > 16) {
binaryString = binaryString.substring(0, 16);
}
if (binaryString[15] === '1') {
// We need to really finish with a 0
binaryString = binaryString.substring(0, 15) + '0';
}
console.assert(binaryString.length === 16);
return parseInt(binaryString, 2);
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRmVhdHVyZUNvbnZlcnRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vbGNzL0ZlYXR1cmVDb252ZXJ0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxXQUFXLE1BQU0sa0JBQWtCLENBQUM7QUFDM0MsT0FBTyxZQUFZLEVBQUUsRUFBd0IsTUFBTSxxQkFBcUIsQ0FBQztBQUN6RSxPQUFPLGVBQWUsTUFBTSxzQkFBc0IsQ0FBQztBQUNuRCxPQUFPLEVBQUMsUUFBUSxJQUFJLHVCQUF1QixFQUFDLE1BQU0sb0JBQW9CLENBQUM7QUFDdkUsT0FBTyxFQUFDLGNBQWMsRUFBRSxTQUFTLEVBQUMsTUFBTSxjQUFjLENBQUM7QUFDdkQsT0FBTyxvQkFBb0IsTUFBTSwyQkFBMkIsQ0FBQztBQUM3RCxPQUFPLEVBQUMsb0JBQW9CLEVBQUUscUJBQXFCLEVBQUUsaUNBQWlDLEVBQUUsbUNBQW1DLEVBQUMsTUFBTSxXQUFXLENBQUM7QUFDOUksT0FBTyxzQkFBc0IsRUFBRSxFQUErQixNQUFNLGtDQUFrQyxDQUFDO0FBQ3ZHLE9BQU8sRUFBQyxNQUFNLEVBQUUsU0FBUyxFQUFDLE1BQU0sV0FBVyxDQUFDO0FBQzVDLE9BQU8sRUFBOFosTUFBTSxRQUFRLENBQUM7QUFLcGIsT0FBTyxFQUEyQyxNQUFNLG1CQUFtQixDQUFDO0FBSTVFLE9BQU8sRUFBQyxRQUFRLElBQUksVUFBVSxFQUE0SSxNQUFNLFlBQVksQ0FBQztBQUU3TCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBbUUvQixNQUFNLENBQUMsT0FBTyxPQUFPLGdCQUFnQjtJQWlCYjtJQWZ0Qjs7T0FFRztJQUNLLG9DQUFvQyxHQUFHLElBQUksQ0FBQyx1QkFBdUIsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFFL0UsMEJBQTBCLEdBQUcsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRSxD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