import { type Billboard, type BillboardCollection, type Cartesian3, type CircleGeometry, type CircleOutlineGeometry, type Color as CSColor, type Geometry as CSGeometry, type GroundPolylinePrimitive, type GroundPrimitive, type HeightReference, type ImageMaterialProperty, type Label, type LabelCollection, type Material, type Matrix4, type Model, type PolygonHierarchy, type Primitive, type PrimitiveCollection, type Scene, } from 'cesium'; import type {Feature, View} from 'ol'; import type {Color as OLColor} from 'ol/color.js'; import type { ColorLike as OLColorLike, PatternDescriptor, } from 'ol/colorlike.js'; import {boundingExtent, getCenter} from 'ol/extent.js'; import { Geometry as OLGeometry, type Circle, type GeometryCollection, type LineString, type MultiLineString, type MultiPoint, type MultiPolygon, type Point, type Polygon, } from 'ol/geom.js'; import {circular as olCreateCircularPolygon} from 'ol/geom/Polygon.js'; import olGeomSimpleGeometry from 'ol/geom/SimpleGeometry.js'; import type ImageLayer from 'ol/layer/Image.js'; import type VectorLayer from 'ol/layer/Vector.js'; import type {ProjectionLike} from 'ol/proj.js'; import OLClusterSource from 'ol/source/Cluster.js'; import VectorSource, {type VectorSourceEvent} from 'ol/source/Vector.js'; import OLStyleIcon from 'ol/style/Icon.js'; import type ImageStyle from 'ol/style/Image.js'; import {type default as Style, type StyleFunction} from 'ol/style/Style.js'; import type Text from 'ol/style/Text.js'; import { convertColorToCesium, ol4326CoordinateArrayToCsCartesians, ol4326CoordinateToCesiumCartesian, olGeometryCloneTo4326, } from './core.js'; import VectorLayerCounterpart, { type OlFeatureToCesiumContext, } from './core/VectorLayerCounterpart.js'; import {getUid, waitReady} from './util.js'; type ModelFromGltfOptions = Parameters[0]; type PrimitiveLayer = VectorLayer | ImageLayer; /** * OL 10 changed the way the VectorLayer is typed. * With "Feature": our code is compatible with OL 6-9 but fails with OL 10. * With "VectorSource": it is compatible with OL 10 but fails with OL 6-9. * This is just a typin issue: our "actual" code does not need to change... * I think some magics with "typescript conditional types" may help here, but how to detect the OL version at typing time? * There is a {VERSION} from 'ol/utils.js symbol, but it is just of type "string", which is not helpful. * For now, I will use "any" here: it is ugly but at least will not put a burden on users of OL versions < 10. */ type BackwardCompatibleFeature = any; // VectorSource; // Feature; declare module 'cesium' { interface Primitive { olLayer: PrimitiveLayer; olFeature: Feature; } interface GroundPolylinePrimitive { olLayer: PrimitiveLayer; olFeature: Feature; _primitive: Primitive; // Missing from types published by Cesium } interface GroundPrimitive { olLayer: PrimitiveLayer; olFeature: Feature; } interface Label { olLayer: PrimitiveLayer; olFeature: Feature; } interface Billboard { olLayer: PrimitiveLayer; olFeature: Feature; } } interface ModelStyle { debugModelMatrix?: Matrix4; cesiumOptions: ModelFromGltfOptions; } interface MaterialAppearanceOptions { flat: boolean; renderState: { depthTest: { enabled: boolean; }; lineWidth?: number; }; } export default class FeatureConverter { /** * Bind once to have a unique function for using as a listener */ private boundOnRemoveOrClearFeatureListener_ = this.onRemoveOrClearFeature_.bind(this); private 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(protected scene: Scene) { this.scene = scene; } /** * @param evt */ private onRemoveOrClearFeature_(evt: VectorSourceEvent) { 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 */ protected setReferenceForPicking( layer: PrimitiveLayer, feature: Feature, primitive: | GroundPolylinePrimitive | GroundPrimitive | Primitive | Label | Billboard, ) { 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 */ protected createColoredPrimitive( layer: PrimitiveLayer, feature: Feature, olGeometry: OLGeometry, geometry: CSGeometry | CircleGeometry, color: CSColor | ImageMaterialProperty, opt_lineWidth?: number, ): Primitive | GroundPrimitive { const createInstance = function ( geometry: CSGeometry | CircleGeometry, color: CSColor | ImageMaterialProperty, ) { const instance = new Cesium.GeometryInstance({ geometry, }); if (color && !(color instanceof Cesium.ImageMaterialProperty)) { instance.attributes = { color: Cesium.ColorGeometryInstanceAttribute.fromColor(color), }; } return instance; }; const options: MaterialAppearanceOptions = { 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: GroundPrimitive | 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. * @param style * @param outline */ protected extractColorFromOlStyle( style: Style | Text, outline: boolean, ): CSColor | ImageMaterialProperty { const fillColor = style.getFill()?.getColor(); const strokeColor = style.getStroke() ? style.getStroke().getColor() : null; let olColor: OLColorLike | OLColor | PatternDescriptor = 'black'; if (strokeColor && outline) { olColor = strokeColor; } else if (fillColor) { olColor = fillColor; } return convertColorToCesium(olColor); } /** * Return the width of stroke from a plain ol style. * @param style * @return {number} */ protected extractLineWidthFromOlStyle(style: Style | Text) { // 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 layer * @param feature * @param olGeometry * @param fillGeometry * @param outlineGeometry * @param olStyle */ protected wrapFillAndOutlineGeometries( layer: PrimitiveLayer, feature: Feature, olGeometry: OLGeometry, fillGeometry: CSGeometry | CircleGeometry, outlineGeometry: CSGeometry | CircleOutlineGeometry, olStyle: Style, ): PrimitiveCollection { 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. * @param layer * @param feature * @param geometry * @param style * @param primitive */ protected addTextStyle( layer: PrimitiveLayer, feature: Feature, geometry: OLGeometry, style: Style, primitive: Primitive | PrimitiveCollection | GroundPolylinePrimitive, ): PrimitiveCollection { 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: BillboardCollection, bbOptions: Parameters[0], layer: PrimitiveLayer, feature: Feature, geometry: OLGeometry, style: Style, ): Billboard { 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 layer * @param feature * @param olGeometry * @param projection * @param olStyle * @api */ olCircleGeometryToCesium( layer: PrimitiveLayer, feature: Feature, olGeometry: Circle, projection: ProjectionLike, olStyle: Style, ): PrimitiveCollection { 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: Cartesian3 = ol4326CoordinateToCesiumCartesian(olCenter); const point: Cartesian3 = ol4326CoordinateToCesiumCartesian(olPoint); // Accurate computation of straight distance const radius = Cesium.Cartesian3.distance(center, point); const fillGeometry = new Cesium.CircleGeometry({ center, radius, height, }); let outlinePrimitive: Primitive | GroundPrimitive | GroundPolylinePrimitive; 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(), ); 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, }); const op = outlinePrimitive; 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. * @param layer * @param feature * @param olGeometry * @param projection * @param olStyle * @api */ olLineStringGeometryToCesium( layer: PrimitiveLayer, feature: Feature, olGeometry: LineString, projection: ProjectionLike, olStyle: Style, ): PrimitiveCollection { olGeometry = olGeometryCloneTo4326(olGeometry, projection); console.assert(olGeometry.getType() == 'LineString'); const positions = ol4326CoordinateArrayToCsCartesians( olGeometry.getCoordinates(), ); const width = this.extractLineWidthFromOlStyle(olStyle); let outlinePrimitive: Primitive | GroundPolylinePrimitive; 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, }); outlinePrimitive = new Cesium.GroundPolylinePrimitive({ appearance, geometryInstances: new Cesium.GeometryInstance({ geometry, }), }); const op = outlinePrimitive; 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, }), }); } this.setReferenceForPicking(layer, feature, outlinePrimitive); return this.addTextStyle( layer, feature, olGeometry, olStyle, outlinePrimitive, ); } /** * Convert an OpenLayers polygon geometry to Cesium. * @param layer * @param feature * @param olGeometry * @param projection * @param olStyle * @api */ olPolygonGeometryToCesium( layer: PrimitiveLayer, feature: Feature, olGeometry: Polygon, projection: ProjectionLike, olStyle: Style, ): PrimitiveCollection { olGeometry = olGeometryCloneTo4326(olGeometry, projection); console.assert(olGeometry.getType() == 'Polygon'); const heightReference = this.getHeightReference(layer, feature, olGeometry); let fillGeometry, outlineGeometry; let outlinePrimitive: GroundPolylinePrimitive; 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: PolygonHierarchy = { positions: [], holes: [], }; const polygonHierarchy: 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: Cartesian3[][] = [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, }), ); } 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); } /** * @param layer * @param feature * @param geometry * @api */ getHeightReference( layer: PrimitiveLayer, feature: Feature, geometry: OLGeometry, ): HeightReference { // 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: PrimitiveLayer, feature: Feature, olGeometry: Point, projection: ProjectionLike, style: Style, imageStyle: ImageStyle, billboards: BillboardCollection, opt_newBillboardCallback: (bb: Billboard) => void, ) { 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: HTMLImageElement) { 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 bbOptions: Parameters[0] = { image: image as any, color, scale: Array.isArray(scale) ? (scale[0] + scale[1]) / 2 : scale, heightReference, position, }; // 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'] = 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: PrimitiveLayer, feature: Feature, olGeometry: Point, projection: ProjectionLike, style: Style, billboards: BillboardCollection, opt_newBillboardCallback?: (bb: Billboard) => void, ): PrimitiveCollection { console.assert(olGeometry.getType() == 'Point'); olGeometry = olGeometryCloneTo4326(olGeometry, projection); let modelPrimitive: PrimitiveCollection = null; const imageStyle = style.getImage(); if (imageStyle) { const olcsModelFunction: () => ModelStyle = olGeometry.get('olcs_model') || feature.get('olcs_model'); if (olcsModelFunction) { modelPrimitive = new Cesium.PrimitiveCollection(); const olcsModel = olcsModelFunction(); const options: ModelFromGltfOptions = 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(), ); } 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: PrimitiveLayer, feature: Feature, geometry: OLGeometry, projection: ProjectionLike, olStyle: Style, billboards: BillboardCollection, opt_newBillboardCallback: (bb: Billboard) => void, ) { // Do not reproject to 4326 now because it will be done later. switch (geometry.getType()) { case 'MultiPoint': { const points = (geometry as MultiPoint).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; } points.forEach((geom) => { console.assert(!!geom); this.olPointGeometryToCesium( layer, feature, geom, projection, olStyle, billboards, opt_newBillboardCallback, ); }); return null; } case 'MultiLineString': { const lineStrings = (geometry as MultiLineString).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 as MultiPolygon).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. * @param layer * @param feature * @param geometry * @param style * @api */ olGeometry4326TextPartToCesium( layer: PrimitiveLayer, feature: Feature, geometry: OLGeometry, style: Text, ): LabelCollection { 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: Parameters[0] = { 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) as CSColor; labelStyle = Cesium.LabelStyle.FILL; } if (style.getStroke()) { options.outlineWidth = this.extractLineWidthFromOlStyle(style); options.outlineColor = this.extractColorFromOlStyle( style, true, ) as CSColor; 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 feature * @param style * @param outline * @api */ olStyleToCesium(feature: Feature, style: Style, outline: boolean): Material { 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, }); } return Cesium.Material.fromType('Color', { color, }); } /** * Compute OpenLayers plain style. * Evaluates style function, blend arrays, get default style. * @param layer * @param feature * @param fallbackStyleFunction * @param resolution * @api */ computePlainStyle( layer: PrimitiveLayer, feature: Feature, fallbackStyleFunction: StyleFunction, resolution: number, ): Style[] { /** * @type {ol.FeatureStyleFunction|undefined} */ const featureStyleFunction = feature.getStyleFunction(); /** * @type {ol.style.Style | Array} */ 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]; } /** * @param feature * @param style * @param opt_geom */ protected getGeometryFromFeature( feature: Feature, style: Style, opt_geom?: OLGeometry, ): OLGeometry | undefined { if (opt_geom) { return opt_geom; } const geom3d: OLGeometry = 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. * @param layer * @param feature * @param style * @param context * @param opt_geom * @api */ olFeatureToCesium( layer: PrimitiveLayer, feature: Feature, style: Style, context: OlFeatureToCesiumContext, opt_geom?: OLGeometry, ): PrimitiveCollection { const geom: OLGeometry = 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: Billboard) { 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(); (geom as GeometryCollection).getGeometriesArray().forEach((geom) => { if (geom) { const prims = this.olFeatureToCesium( layer, feature, style, context, geom, ); if (prims) { primitives.add(prims); } } }); return primitives; case 'Point': const bbs = context.billboards; const result = this.olPointGeometryToCesium( layer, feature, geom as Point, proj, style, bbs, newBillboardAddedCallback, ); if (!result) { // no wrapping primitive return null; } return result; case 'Circle': return this.olCircleGeometryToCesium( layer, feature, geom as Circle, proj, style, ); case 'LineString': return this.olLineStringGeometryToCesium( layer, feature, geom as LineString, proj, style, ); case 'Polygon': return this.olPolygonGeometryToCesium( layer, feature, geom as Polygon, proj, style, ); case 'MultiPoint': return ( this.olMultiGeometryToCesium( layer, feature, geom as MultiPoint, proj, style, context.billboards, newBillboardAddedCallback, ) || null ); case 'MultiLineString': return ( this.olMultiGeometryToCesium( layer, feature, geom as MultiLineString, proj, style, context.billboards, newBillboardAddedCallback, ) || null ); case 'MultiPolygon': return ( this.olMultiGeometryToCesium( layer, feature, geom as MultiPolygon, proj, style, context.billboards, newBillboardAddedCallback, ) || null ); 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 olLayer * @param olView * @param featurePrimitiveMap * @api */ olVectorLayerToCesium( olLayer: VectorLayer, olView: View, featurePrimitiveMap: Record, ): VectorLayerCounterpart { 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: StyleFunction | undefined = olLayer.getStyleFunction(); const styles = this.computePlainStyle( olLayer, feature, layerStyle, resolution, ); if (!styles || !styles.length) { // only 'render' features with a style continue; } let primitives: PrimitiveCollection = 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 layer * @param view * @param feature * @param context * @api */ convert( layer: VectorLayer, view: View, feature: Feature, context: OlFeatureToCesiumContext, ): PrimitiveCollection { 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: number[]): number { 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); }