UNPKG

olcs

Version:

OpenLayers Cesium integration and plugin library

1,120 lines 88.8 kB
import {} from 'cesium'; import { boundingExtent, getCenter } from 'ol/extent.js'; import { Geometry as OLGeometry, } from 'ol/geom.js'; import { circular as olCreateCircularPolygon } from 'ol/geom/Polygon.js'; import olGeomSimpleGeometry from 'ol/geom/SimpleGeometry.js'; import OLClusterSource from 'ol/source/Cluster.js'; import VectorSource, {} from 'ol/source/Vector.js'; import OLStyleIcon from 'ol/style/Icon.js'; import {} from 'ol/style/Style.js'; import { convertColorToCesium, ol4326CoordinateArrayToCsCartesians, ol4326CoordinateToCesiumCartesian, olGeometryCloneTo4326, } from './core.js'; import VectorLayerCounterpart, {} from './core/VectorLayerCounterpart.js'; import { getUid, waitReady } from './util.js'; 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 instance = new Cesium.GeometryInstance({ geometry, }); 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. * @param style * @param outline */ 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; } return convertColorToCesium(olColor); } /** * 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. * @param layer * @param feature * @param olGeometry * @param fillGeometry * @param outlineGeometry * @param olStyle */ 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. * @param layer * @param feature * @param geometry * @param style * @param 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. * @param layer * @param feature * @param olGeometry * @param projection * @param olStyle * @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 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()); 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, feature, olGeometry, projection, olStyle) { olGeometry = olGeometryCloneTo4326(olGeometry, projection); console.assert(olGeometry.getType() == 'LineString'); const positions = ol4326CoordinateArrayToCsCartesians(olGeometry.getCoordinates()); const width = this.extractLineWidthFromOlStyle(olStyle); 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, }); 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, 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; 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, })); } 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, 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 bbOptions = { image: image, 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, 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()); } 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; } 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. * @param layer * @param feature * @param geometry * @param style * @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. * @param feature * @param style * @param outline * @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, }); } 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, 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]; } /** * @param feature * @param style * @param opt_geom */ 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. * @param layer * @param feature * @param style * @param context * @param opt_geom * @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]; } }; switch (geom.getType()) { case 'GeometryCollection': const primitives = new Cesium.PrimitiveCollection(); geom.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, proj, style, bbs, newBillboardAddedCallback); if (!result) { // no wrapping primitive return null; } return result; case 'Circle': return this.olCircleGeometryToCesium(layer, feature, geom, proj, style); case 'LineString': return this.olLineStringGeometryToCesium(layer, feature, geom, proj, style); case 'Polygon': return this.olPolygonGeometryToCesium(layer, feature, geom, proj, style); case 'MultiPoint': return (this.olMultiGeometryToCesium(layer, feature, geom, proj, style, context.billboards, newBillboardAddedCallback) || null); case 'MultiLineString': return (this.olMultiGeometryToCesium(layer, feature, geom, proj, style, context.billboards, newBillboardAddedCallback) || null); case 'MultiPolygon': return (this.olMultiGeometryToCesium(layer, feature, geom, 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, 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. * @param layer * @param view * @param feature * @param context * @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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRmVhdHVyZUNvbnZlcnRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vbGNzL0ZlYXR1cmVDb252ZXJ0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQXFCTixNQUFNLFFBQVEsQ0FBQztBQU9oQixPQUFPLEVBQUMsY0FBYyxFQUFFLFNBQVMsRUFBQyxNQUFNLGNBQWMsQ0FBQztBQUN2RCxPQUFPLEVBQ0wsUUFBUSxJQUFJLFVBQVUsR0FTdkIsTUFBTSxZQUFZLENBQUM7QUFDcEIsT0FBTyxFQUFDLFFBQVEsSUFBSSx1QkFBdUIsRUFBQyxNQUFNLG9CQUFvQixDQUFDO0FBQ3ZFLE9BQU8sb0JBQW9CLE1BQU0sMkJBQTJCLENBQUM7QUFJN0QsT0FBTyxlQUFlLE1BQU0sc0JBQXNCLENBQUM7QUFDbkQsT0FBTyxZQUFZLEVBQUUsRUFBd0IsTUFBTSxxQkFBcUIsQ0FBQztBQUN6RSxPQUFPLFdBQVcsTUFBTSxrQkFBa0IsQ0FBQztBQUUzQyxPQUFPLEVBQTJDLE1BQU0sbUJBQW1CLENBQUM7QUFFNUUsT0FBTyxFQUNMLG9CQUFvQixFQUNwQixtQ0FBbUMsRUFDbkMsaUNBQWlDLEVBQ2pDLHFCQUFxQixHQUN0QixNQUFNLFdBQVcsQ0FBQztBQUNuQixPQUFPLHNCQUFzQixFQUFFLEVBRTlCLE1BQU0sa0NBQWtDLENBQUM7QUFDMUMsT0FBTyxFQUFDLE1BQU0sRUFBRSxTQUFTLEVBQUMsTUFBTSxXQUFXLENBQUM7QUE0RDVDLE1BQU0sQ0FBQyxPQUFPLE9BQU8sZ0JBQWdCO0lBaUJiO0lBaEJ0Qjs7T0FFRztJQUNLLG9DQUFvQyxHQUMxQyxJQUFJLENBQUMsdUJBQXVCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBRWxDLDBCQUEwQixHQUFHLElBQUksTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBRXJFOzs7Ozs7O09BT0c7SUFDSCxZQUFz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