/** * @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|Array>} 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 scale = imageStyle.getScale(); const heightReference = this.getHeightReference(layer, feature, olGeometry); const bbOptions = /** @type {Cesium.optionsBillboardCollectionAdd} */ ({ // always update Cesium externs before adding a property image, color, 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) { bbOptions.pixelOffset = new Cesium.Cartesian2((image.width / 2 - anchor[0]) * scale, (image.height / 2 - anchor[1]) * scale); } } 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.} 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.} */ 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.} 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 || !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;