UNPKG

ev-olcs

Version:

OpenLayers Cesium integration and plugin library

1,090 lines 89.9 kB
import OLStyleIcon from 'ol/style/Icon.js'; import VectorSource, {} from 'ol/source/Vector.js'; import OLClusterSource from 'ol/source/Cluster.js'; import { circular as olCreateCircularPolygon } from 'ol/geom/Polygon.js'; import { boundingExtent, getCenter } from 'ol/extent.js'; import olGeomSimpleGeometry from 'ol/geom/SimpleGeometry.js'; import { convertColorToCesium, olGeometryCloneTo4326, ol4326CoordinateToCesiumCartesian, ol4326CoordinateArrayToCsCartesians } from './core.js'; import VectorLayerCounterpart, {} from './core/VectorLayerCounterpart.js'; import { getUid, waitReady } from './util.js'; import {} from 'cesium'; import {} from 'ol/style/Style.js'; import { Geometry as OLGeometry } from 'ol/geom.js'; import { clone } from 'cesium'; export default class FeatureConverter { scene; /** * Bind once to have a unique function for using as a listener */ boundOnRemoveOrClearFeatureListener_ = this.onRemoveOrClearFeature_.bind(this); defaultBillboardEyeOffset_ = new Cesium.Cartesian3(0, 0, 10); /** * Concrete base class for converting from OpenLayers3 vectors to Cesium * primitives. * Extending this class is possible provided that the extending class and * the library are compiled together by the closure compiler. * @param scene Cesium scene. * @api */ constructor(scene) { this.scene = scene; this.scene = scene; } /** * @param evt */ onRemoveOrClearFeature_(evt) { const source = evt.target; console.assert(source instanceof VectorSource); const cancellers = source['olcs_cancellers']; if (cancellers) { const feature = evt.feature; if (feature) { // remove const id = getUid(feature); const canceller = cancellers[id]; if (canceller) { canceller(); delete cancellers[id]; } } else { // clear for (const key in cancellers) { if (cancellers.hasOwnProperty(key)) { cancellers[key](); } } source['olcs_cancellers'] = {}; } } } /** * @param layer * @param feature OpenLayers feature. * @param primitive */ setReferenceForPicking(layer, feature, primitive) { primitive.olLayer = layer; primitive.olFeature = feature; } /** * Basics primitive creation using a color attribute. * Note that Cesium has 'interior' and outline geometries. * @param layer * @param feature OpenLayers feature. * @param olGeometry OpenLayers geometry. * @param geometry * @param color * @param opt_lineWidth * @return primitive */ createColoredPrimitive(layer, feature, olGeometry, geometry, color, opt_lineWidth) { const createInstance = function (geometry, color) { const prop = clone(feature.getProperties()); Reflect.deleteProperty(prop, "geometry"); const instance = new Cesium.GeometryInstance({ geometry, id: prop // Set id here on GeometryInstance }); if (color && !(color instanceof Cesium.ImageMaterialProperty)) { instance.attributes = { color: Cesium.ColorGeometryInstanceAttribute.fromColor(color) }; } return instance; }; const options = { flat: true, // work with all geometries renderState: { depthTest: { enabled: true } } }; if (opt_lineWidth !== undefined) { options.renderState.lineWidth = opt_lineWidth; } const instances = createInstance(geometry, color); const heightReference = this.getHeightReference(layer, feature, olGeometry); let primitive; if (heightReference === Cesium.HeightReference.CLAMP_TO_GROUND) { if (!('createShadowVolume' in instances.geometry.constructor)) { // This is not a ground geometry return null; } primitive = new Cesium.GroundPrimitive({ geometryInstances: instances }); } else { primitive = new Cesium.Primitive({ geometryInstances: instances }); } if (color instanceof Cesium.ImageMaterialProperty) { // FIXME: we created stylings which are not time related // What should we pass here? // @ts-ignore const dataUri = color.image.getValue().toDataURL(); primitive.appearance = new Cesium.MaterialAppearance({ flat: true, renderState: { depthTest: { enabled: true, } }, material: new Cesium.Material({ fabric: { type: 'Image', uniforms: { image: dataUri } } }) }); } else { primitive.appearance = new Cesium.MaterialAppearance({ ...options, material: new Cesium.Material({ translucent: color.alpha !== 1, fabric: { type: 'Color', uniforms: { color, } } }) }); if (primitive instanceof Cesium.Primitive && (feature.get('olcs_shadows') || layer.get('olcs_shadows'))) { primitive.shadows = 1; } } this.setReferenceForPicking(layer, feature, primitive); return primitive; } /** * Return the fill or stroke color from a plain ol style. */ extractColorFromOlStyle(style, outline) { const fillColor = style.getFill()?.getColor(); const strokeColor = style.getStroke() ? style.getStroke().getColor() : null; let olColor = 'black'; if (strokeColor && outline) { olColor = strokeColor; } else if (fillColor) { olColor = fillColor; } const csColor = convertColorToCesium(olColor); if (csColor instanceof Cesium.ImageMaterialProperty) { return csColor; } if ('red' in csColor) { return csColor; } else { // Fallback to black if that was not a plain color return Cesium.Color.BLACK; } } /** * Return the width of stroke from a plain ol style. * @param style * @return {number} */ extractLineWidthFromOlStyle(style) { // Handling of line width WebGL limitations is handled by Cesium. const width = style.getStroke() ? style.getStroke().getWidth() : undefined; return width !== undefined ? width : 1; } /** * Create a primitive collection out of two Cesium geometries. * Only the OpenLayers style colors will be used. */ wrapFillAndOutlineGeometries(layer, feature, olGeometry, fillGeometry, outlineGeometry, olStyle) { const fillColor = this.extractColorFromOlStyle(olStyle, false); const outlineColor = this.extractColorFromOlStyle(olStyle, true); const primitives = new Cesium.PrimitiveCollection(); if (olStyle.getFill()) { const p1 = this.createColoredPrimitive(layer, feature, olGeometry, fillGeometry, fillColor); console.assert(!!p1); primitives.add(p1); } if (olStyle.getStroke() && outlineGeometry) { const width = this.extractLineWidthFromOlStyle(olStyle); const p2 = this.createColoredPrimitive(layer, feature, olGeometry, outlineGeometry, outlineColor, width); if (p2) { // Some outline geometries are not supported by Cesium in clamp to ground // mode. These primitives are skipped. primitives.add(p2); } } return primitives; } // Geometry converters // FIXME: would make more sense to only accept primitive collection. /** * Create a Cesium primitive if style has a text component. * Eventually return a PrimitiveCollection including current primitive. */ addTextStyle(layer, feature, geometry, style, primitive) { let primitives; if (!(primitive instanceof Cesium.PrimitiveCollection)) { primitives = new Cesium.PrimitiveCollection(); primitives.add(primitive); } else { primitives = primitive; } if (!style.getText()) { return primitives; } const text = /** @type {!ol.style.Text} */ (style.getText()); const label = this.olGeometry4326TextPartToCesium(layer, feature, geometry, text); if (label) { primitives.add(label); } return primitives; } /** * Add a billboard to a Cesium.BillboardCollection. * Overriding this wrapper allows manipulating the billboard options. * @param billboards * @param bbOptions * @param layer * @param feature OpenLayers feature. * @param geometry * @param style * @return newly created billboard * @api */ csAddBillboard(billboards, bbOptions, layer, feature, geometry, style) { if (!bbOptions.eyeOffset) { bbOptions.eyeOffset = this.defaultBillboardEyeOffset_; } const bb = billboards.add(bbOptions); this.setReferenceForPicking(layer, feature, bb); return bb; } /** * Convert an OpenLayers circle geometry to Cesium. * @api */ olCircleGeometryToCesium(layer, feature, olGeometry, projection, olStyle) { olGeometry = olGeometryCloneTo4326(olGeometry, projection); console.assert(olGeometry.getType() == 'Circle'); // ol.Coordinate const olCenter = olGeometry.getCenter(); const height = olCenter.length == 3 ? olCenter[2] : 0.0; const olPoint = olCenter.slice(); olPoint[0] += olGeometry.getRadius(); // Cesium const center = ol4326CoordinateToCesiumCartesian(olCenter); const point = ol4326CoordinateToCesiumCartesian(olPoint); // Accurate computation of straight distance const radius = Cesium.Cartesian3.distance(center, point); const prop = clone(feature.getProperties()); Reflect.deleteProperty(prop, "geometry"); const fillGeometry = new Cesium.CircleGeometry({ center, radius, height }); let outlinePrimitive; let outlineGeometry; if (this.getHeightReference(layer, feature, olGeometry) === Cesium.HeightReference.CLAMP_TO_GROUND) { const width = this.extractLineWidthFromOlStyle(olStyle); if (width) { const circlePolygon = olCreateCircularPolygon(olGeometry.getCenter(), radius); const positions = ol4326CoordinateArrayToCsCartesians(circlePolygon.getLinearRing(0).getCoordinates()); const op = outlinePrimitive = new Cesium.GroundPolylinePrimitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: new Cesium.GroundPolylineGeometry({ positions, width }), id: prop // Set id for GroundPolyline }), appearance: new Cesium.PolylineMaterialAppearance({ material: this.olStyleToCesium(feature, olStyle, true), }), classificationType: Cesium.ClassificationType.TERRAIN, }); waitReady(outlinePrimitive).then(() => { this.setReferenceForPicking(layer, feature, op._primitive); }); } } else { outlineGeometry = new Cesium.CircleOutlineGeometry({ center, radius, extrudedHeight: height, height }); } const primitives = this.wrapFillAndOutlineGeometries(layer, feature, olGeometry, fillGeometry, outlineGeometry, olStyle); if (outlinePrimitive) { primitives.add(outlinePrimitive); } return this.addTextStyle(layer, feature, olGeometry, olStyle, primitives); } /** * Convert an OpenLayers line string geometry to Cesium. * @api */ olLineStringGeometryToCesium(layer, feature, olGeometry, projection, olStyle) { olGeometry = olGeometryCloneTo4326(olGeometry, projection); console.assert(olGeometry.getType() == 'LineString'); const positions = ol4326CoordinateArrayToCsCartesians(olGeometry.getCoordinates()); const width = this.extractLineWidthFromOlStyle(olStyle); const prop = clone(feature.getProperties()); Reflect.deleteProperty(prop, "geometry"); let outlinePrimitive; const heightReference = this.getHeightReference(layer, feature, olGeometry); const appearance = new Cesium.PolylineMaterialAppearance({ material: this.olStyleToCesium(feature, olStyle, true) }); if (heightReference === Cesium.HeightReference.CLAMP_TO_GROUND) { const geometry = new Cesium.GroundPolylineGeometry({ positions, width, }); const op = outlinePrimitive = new Cesium.GroundPolylinePrimitive({ appearance, geometryInstances: new Cesium.GeometryInstance({ geometry, id: prop // Set id here for GroundPolyline }) }); waitReady(outlinePrimitive).then(() => { this.setReferenceForPicking(layer, feature, op._primitive); }); } else { const geometry = new Cesium.PolylineGeometry({ positions, width, vertexFormat: appearance.vertexFormat }); outlinePrimitive = new Cesium.Primitive({ appearance, geometryInstances: new Cesium.GeometryInstance({ geometry, id: prop // Set id here for normal Polyline }), }); } this.setReferenceForPicking(layer, feature, outlinePrimitive); return this.addTextStyle(layer, feature, olGeometry, olStyle, outlinePrimitive); } /** * Convert an OpenLayers polygon geometry to Cesium. * @api */ olPolygonGeometryToCesium(layer, feature, olGeometry, projection, olStyle) { olGeometry = olGeometryCloneTo4326(olGeometry, projection); console.assert(olGeometry.getType() == 'Polygon'); const heightReference = this.getHeightReference(layer, feature, olGeometry); let fillGeometry, outlineGeometry; let outlinePrimitive; const prop = clone(feature.getProperties()); Reflect.deleteProperty(prop, "geometry"); if ((olGeometry.getCoordinates()[0].length == 5) && (feature.get('olcs_polygon_kind') === 'rectangle')) { // Create a rectangle according to the longitude and latitude curves const coordinates = olGeometry.getCoordinates()[0]; // Extract the West, South, East, North coordinates const extent = boundingExtent(coordinates); const rectangle = Cesium.Rectangle.fromDegrees(extent[0], extent[1], extent[2], extent[3]); // Extract the average height of the vertices let maxHeight = 0.0; if (coordinates[0].length == 3) { for (let c = 0; c < coordinates.length; c++) { maxHeight = Math.max(maxHeight, coordinates[c][2]); } } const featureExtrudedHeight = feature.get('olcs_extruded_height'); // Render the cartographic rectangle fillGeometry = new Cesium.RectangleGeometry({ ellipsoid: Cesium.Ellipsoid.WGS84, rectangle, height: maxHeight, extrudedHeight: featureExtrudedHeight, }); outlineGeometry = new Cesium.RectangleOutlineGeometry({ ellipsoid: Cesium.Ellipsoid.WGS84, rectangle, height: maxHeight, extrudedHeight: featureExtrudedHeight, }); } else { const rings = olGeometry.getLinearRings(); const hierarchy = { positions: [], holes: [], }; const polygonHierarchy = hierarchy; console.assert(rings.length > 0); for (let i = 0; i < rings.length; ++i) { const olPos = rings[i].getCoordinates(); const positions = ol4326CoordinateArrayToCsCartesians(olPos); console.assert(positions && positions.length > 0); if (i === 0) { hierarchy.positions = positions; } else { hierarchy.holes.push({ positions, holes: [], }); } } const featureExtrudedHeight = feature.get('olcs_extruded_height'); fillGeometry = new Cesium.PolygonGeometry({ polygonHierarchy, perPositionHeight: true, extrudedHeight: featureExtrudedHeight, }); // Since Cesium doesn't yet support Polygon outlines on terrain yet (coming soon...?) // we don't create an outline geometry if clamped, but instead do the polyline method // for each ring. Most of this code should be removeable when Cesium adds // support for Polygon outlines on terrain. if (heightReference === Cesium.HeightReference.CLAMP_TO_GROUND) { const width = this.extractLineWidthFromOlStyle(olStyle); if (width > 0) { const positions = [hierarchy.positions]; if (hierarchy.holes) { for (let i = 0; i < hierarchy.holes.length; ++i) { positions.push(hierarchy.holes[i].positions); } } const appearance = new Cesium.PolylineMaterialAppearance({ material: this.olStyleToCesium(feature, olStyle, true) }); const geometryInstances = []; for (const linePositions of positions) { const polylineGeometry = new Cesium.GroundPolylineGeometry({ positions: linePositions, width }); geometryInstances.push(new Cesium.GeometryInstance({ geometry: polylineGeometry, id: prop // Set id for outline })); } outlinePrimitive = new Cesium.GroundPolylinePrimitive({ appearance, geometryInstances }); waitReady(outlinePrimitive).then(() => { this.setReferenceForPicking(layer, feature, outlinePrimitive._primitive); }); } } else { // Actually do the normal polygon thing. This should end the removable // section of code described above. outlineGeometry = new Cesium.PolygonOutlineGeometry({ polygonHierarchy: hierarchy, perPositionHeight: true, extrudedHeight: featureExtrudedHeight, }); } } const primitives = this.wrapFillAndOutlineGeometries(layer, feature, olGeometry, fillGeometry, outlineGeometry, olStyle); if (outlinePrimitive) { primitives.add(outlinePrimitive); } return this.addTextStyle(layer, feature, olGeometry, olStyle, primitives); } /** * @api */ getHeightReference(layer, feature, geometry) { // Read from the geometry let altitudeMode = geometry.get('altitudeMode'); // Or from the feature if (altitudeMode === undefined) { altitudeMode = feature.get('altitudeMode'); } // Or from the layer if (altitudeMode === undefined) { altitudeMode = layer.get('altitudeMode'); } let heightReference = Cesium.HeightReference.NONE; if (altitudeMode === 'clampToGround') { heightReference = Cesium.HeightReference.CLAMP_TO_GROUND; } else if (altitudeMode === 'relativeToGround') { heightReference = Cesium.HeightReference.RELATIVE_TO_GROUND; } return heightReference; } /** * Convert a point geometry to a Cesium BillboardCollection. * @param {ol.layer.Vector|ol.layer.Image} layer * @param {!ol.Feature} feature OpenLayers feature.. * @param {!ol.geom.Point} olGeometry OpenLayers point geometry. * @param {!ol.ProjectionLike} projection * @param {!ol.style.Style} style * @param {!ol.style.Image} imageStyle * @param {!Cesium.BillboardCollection} billboards * @param {function(!Cesium.Billboard)=} opt_newBillboardCallback Called when the new billboard is added. * @api */ createBillboardFromImage(layer, feature, olGeometry, projection, style, imageStyle, billboards, opt_newBillboardCallback) { if (imageStyle instanceof OLStyleIcon) { // make sure the image is scheduled for load imageStyle.load(); } const image = imageStyle.getImage(1); // get normal density const isImageLoaded = function (image) { return image.src != '' && image.naturalHeight != 0 && image.naturalWidth != 0 && image.complete; }; const reallyCreateBillboard = (function () { if (!image) { return; } if (!(image instanceof HTMLCanvasElement || image instanceof Image || image instanceof HTMLImageElement)) { return; } const center = olGeometry.getCoordinates(); const position = ol4326CoordinateToCesiumCartesian(center); let color; const opacity = imageStyle.getOpacity(); if (opacity !== undefined) { color = new Cesium.Color(1.0, 1.0, 1.0, opacity); } const scale = imageStyle.getScale(); const heightReference = this.getHeightReference(layer, feature, olGeometry); const prop = clone(feature.getProperties()); Reflect.deleteProperty(prop, "geometry"); // remove geometry from getProperties const bbOptions = { image: image, color, scale: Array.isArray(scale) ? (scale[0] + scale[1]) / 2 : scale, heightReference, position, id: prop }; // merge in cesium options from openlayers feature Object.assign(bbOptions, feature.get('cesiumOptions')); if (imageStyle instanceof OLStyleIcon) { const anchor = imageStyle.getAnchor(); if (anchor) { const xScale = (Array.isArray(scale) ? scale[0] : scale); const yScale = (Array.isArray(scale) ? scale[1] : scale); bbOptions.pixelOffset = new Cesium.Cartesian2((image.width / 2 - anchor[0]) * xScale, (image.height / 2 - anchor[1]) * yScale); } } const bb = this.csAddBillboard(billboards, bbOptions, layer, feature, olGeometry, style); if (opt_newBillboardCallback) { opt_newBillboardCallback(bb); } }).bind(this); if (image instanceof Image && !isImageLoaded(image)) { // Cesium requires the image to be loaded let cancelled = false; const source = layer.getSource(); const canceller = function () { cancelled = true; }; source.on(['removefeature', 'clear'], this.boundOnRemoveOrClearFeatureListener_); let cancellers = source['olcs_cancellers']; if (!cancellers) { cancellers = source['olcs_cancellers'] = {}; } const fuid = getUid(feature); if (cancellers[fuid]) { // When the feature change quickly, a canceller may still be present so // we cancel it here to prevent creation of a billboard. cancellers[fuid](); } cancellers[fuid] = canceller; const listener = function () { image.removeEventListener('load', listener); if (!billboards.isDestroyed() && !cancelled) { // Create billboard if the feature is still displayed on the map. reallyCreateBillboard(); } }; image.addEventListener('load', listener); } else { reallyCreateBillboard(); } } /** * Convert a point geometry to a Cesium BillboardCollection. * @param layer * @param feature OpenLayers feature.. * @param olGeometry OpenLayers point geometry. * @param projection * @param style * @param billboards * @param opt_newBillboardCallback Called when the new billboard is added. * @return primitives * @api */ olPointGeometryToCesium(layer, feature, olGeometry, projection, style, billboards, opt_newBillboardCallback) { console.assert(olGeometry.getType() == 'Point'); olGeometry = olGeometryCloneTo4326(olGeometry, projection); let modelPrimitive = null; const imageStyle = style.getImage(); if (imageStyle) { const olcsModelFunction = olGeometry.get('olcs_model') || feature.get('olcs_model'); if (olcsModelFunction) { modelPrimitive = new Cesium.PrimitiveCollection(); const olcsModel = olcsModelFunction(); const options = Object.assign({}, { scene: this.scene }, olcsModel.cesiumOptions); if ('fromGltf' in Cesium.Model) { // pre Cesium v107 // @ts-ignore const model = Cesium.Model.fromGltf(options); modelPrimitive.add(model); } else { Cesium.Model.fromGltfAsync(options).then((model) => { modelPrimitive.add(model); }); } if (olcsModel.debugModelMatrix) { modelPrimitive.add(new Cesium.DebugModelMatrixPrimitive({ modelMatrix: olcsModel.debugModelMatrix })); } } else { this.createBillboardFromImage(layer, feature, olGeometry, projection, style, imageStyle, billboards, opt_newBillboardCallback); } } if (style.getText()) { return this.addTextStyle(layer, feature, olGeometry, style, modelPrimitive || new Cesium.Primitive()); } else { return modelPrimitive; } } /** * Convert an OpenLayers multi-something geometry to Cesium. * @param {ol.layer.Vector|ol.layer.Image} layer * @param {!ol.Feature} feature OpenLayers feature.. * @param {!ol.geom.Geometry} geometry OpenLayers geometry. * @param {!ol.ProjectionLike} projection * @param {!ol.style.Style} olStyle * @param {!Cesium.BillboardCollection} billboards * @param {function(!Cesium.Billboard)=} opt_newBillboardCallback Called when * the new billboard is added. * @return {Cesium.Primitive} primitives * @api */ olMultiGeometryToCesium(layer, feature, geometry, projection, olStyle, billboards, opt_newBillboardCallback) { // Do not reproject to 4326 now because it will be done later. switch (geometry.getType()) { case 'MultiPoint': { const points = geometry.getPoints(); if (olStyle.getText()) { const primitives = new Cesium.PrimitiveCollection(); points.forEach((geom) => { console.assert(geom); const result = this.olPointGeometryToCesium(layer, feature, geom, projection, olStyle, billboards, opt_newBillboardCallback); if (result) { primitives.add(result); } }); return primitives; } else { points.forEach((geom) => { console.assert(geom); this.olPointGeometryToCesium(layer, feature, geom, projection, olStyle, billboards, opt_newBillboardCallback); }); return null; } } case 'MultiLineString': { const lineStrings = geometry.getLineStrings(); // FIXME: would be better to combine all child geometries in one primitive // instead we create n primitives for simplicity. const primitives = new Cesium.PrimitiveCollection(); lineStrings.forEach((geom) => { const p = this.olLineStringGeometryToCesium(layer, feature, geom, projection, olStyle); primitives.add(p); }); return primitives; } case 'MultiPolygon': { const polygons = geometry.getPolygons(); // FIXME: would be better to combine all child geometries in one primitive // instead we create n primitives for simplicity. const primitives = new Cesium.PrimitiveCollection(); polygons.forEach((geom) => { const p = this.olPolygonGeometryToCesium(layer, feature, geom, projection, olStyle); primitives.add(p); }); return primitives; } default: console.assert(false, `Unhandled multi geometry type${geometry.getType()}`); } } /** * Convert an OpenLayers text style to Cesium. * @api */ olGeometry4326TextPartToCesium(layer, feature, geometry, style) { const text = style.getText(); if (!text) { return null; } const labels = new Cesium.LabelCollection({ scene: this.scene }); // TODO: export and use the text draw position from OpenLayers . // See src/ol/render/vector.js const extentCenter = getCenter(geometry.getExtent()); if (geometry instanceof olGeomSimpleGeometry) { const first = geometry.getFirstCoordinate(); extentCenter[2] = first.length == 3 ? first[2] : 0.0; } const options = { position: ol4326CoordinateToCesiumCartesian(extentCenter) }; options.text = Array.isArray(text) ? text.join(' ') : text; options.heightReference = this.getHeightReference(layer, feature, geometry); const offsetX = style.getOffsetX(); const offsetY = style.getOffsetY(); if (offsetX != 0 || offsetY != 0) { const offset = new Cesium.Cartesian2(offsetX, offsetY); options.pixelOffset = offset; } options.font = style.getFont() || '10px sans-serif'; // OpenLayers default let labelStyle = undefined; if (style.getFill()) { options.fillColor = this.extractColorFromOlStyle(style, false); labelStyle = Cesium.LabelStyle.FILL; } if (style.getStroke()) { options.outlineWidth = this.extractLineWidthFromOlStyle(style); options.outlineColor = this.extractColorFromOlStyle(style, true); labelStyle = Cesium.LabelStyle.OUTLINE; } if (style.getFill() && style.getStroke()) { labelStyle = Cesium.LabelStyle.FILL_AND_OUTLINE; } options.style = labelStyle; let horizontalOrigin; switch (style.getTextAlign()) { case 'left': horizontalOrigin = Cesium.HorizontalOrigin.LEFT; break; case 'right': horizontalOrigin = Cesium.HorizontalOrigin.RIGHT; break; case 'center': default: horizontalOrigin = Cesium.HorizontalOrigin.CENTER; } options.horizontalOrigin = horizontalOrigin; if (style.getTextBaseline()) { let verticalOrigin; switch (style.getTextBaseline()) { case 'top': verticalOrigin = Cesium.VerticalOrigin.TOP; break; case 'middle': verticalOrigin = Cesium.VerticalOrigin.CENTER; break; case 'bottom': verticalOrigin = Cesium.VerticalOrigin.BOTTOM; break; case 'alphabetic': verticalOrigin = Cesium.VerticalOrigin.TOP; break; case 'hanging': verticalOrigin = Cesium.VerticalOrigin.BOTTOM; break; default: console.assert(false, `unhandled baseline ${style.getTextBaseline()}`); } options.verticalOrigin = verticalOrigin; } const l = labels.add(options); this.setReferenceForPicking(layer, feature, l); return labels; } /** * Convert an OpenLayers style to a Cesium Material. * @api */ olStyleToCesium(feature, style, outline) { const fill = style.getFill(); const stroke = style.getStroke(); if ((outline && !stroke) || (!outline && !fill)) { return null; // FIXME use a default style? Developer error? } const olColor = outline ? stroke.getColor() : fill.getColor(); const color = convertColorToCesium(olColor); const lineDash = stroke.getLineDash(); if (outline && lineDash) { return Cesium.Material.fromType('PolylineDash', { dashPattern: dashPattern(lineDash), color }); } else { return Cesium.Material.fromType('Color', { color }); } } /** * Compute OpenLayers plain style. * Evaluates style function, blend arrays, get default style. * @api */ computePlainStyle(layer, feature, fallbackStyleFunction, resolution) { /** * @type {ol.FeatureStyleFunction|undefined} */ const featureStyleFunction = feature.getStyleFunction(); /** * @type {ol.style.Style|Array.<ol.style.Style>} */ let style = null; if (featureStyleFunction) { style = featureStyleFunction(feature, resolution); } if (!style && fallbackStyleFunction) { style = fallbackStyleFunction(feature, resolution); } if (!style) { // The feature must not be displayed return null; } // FIXME combine materials as in cesium-materials-pack? // then this function must return a custom material // More simply, could blend the colors like described in // http://en.wikipedia.org/wiki/Alpha_compositing return Array.isArray(style) ? style : [style]; } /** */ getGeometryFromFeature(feature, style, opt_geom) { if (opt_geom) { return opt_geom; } const geom3d = feature.get('olcs_3d_geometry'); if (geom3d && geom3d instanceof OLGeometry) { return geom3d; } if (style) { const geomFuncRes = style.getGeometryFunction()(feature); if (geomFuncRes instanceof OLGeometry) { return geomFuncRes; } } return feature.getGeometry(); } /** * Convert one OpenLayers feature up to a collection of Cesium primitives. * @api */ olFeatureToCesium(layer, feature, style, context, opt_geom) { const geom = this.getGeometryFromFeature(feature, style, opt_geom); if (!geom) { // OpenLayers features may not have a geometry // See http://geojson.org/geojson-spec.html#feature-objects return null; } const proj = context.projection; const newBillboardAddedCallback = function (bb) { const featureBb = context.featureToCesiumMap[getUid(feature)]; if (featureBb instanceof Array) { featureBb.push(bb); } else { context.featureToCesiumMap[getUid(feature)] = [bb]; } }; let primitives; switch (geom.getType()) { case 'GeometryCollection': primitives = new Cesium.PrimitiveCollection(); geom.getGeometriesArray().forEach((geom) => { if (geom) { const prims = this.olFeatureToCesium(layer, feature, style, context, geom); if (prims) { primitives.add(prims); } } }); break; case 'Point': const bbs = context.billboards; primitives = this.olPointGeometryToCesium(layer, feature, geom, proj, style, bbs, newBillboardAddedCallback); break; case 'Circle': primitives = this.olCircleGeometryToCesium(layer, feature, geom, proj, style); break; case 'LineString': primitives = this.olLineStringGeometryToCesium(layer, feature, geom, proj, style); break; case 'Polygon': primitives = this.olPolygonGeometryToCesium(layer, feature, geom, proj, style); break; case 'MultiPoint': primitives = this.olMultiGeometryToCesium(layer, feature, geom, proj, style, context.billboards, newBillboardAddedCallback) || null; break; case 'MultiLineString': primitives = this.olMultiGeometryToCesium(layer, feature, geom, proj, style, context.billboards, newBillboardAddedCallback) || null; break; case 'MultiPolygon': primitives = this.olMultiGeometryToCesium(layer, feature, geom, proj, style, context.billboards, newBillboardAddedCallback) || null; break; case 'LinearRing': throw new Error('LinearRing should only be part of polygon.'); default: throw new Error(`Ol geom type not handled : ${geom.getType()}`); } return primitives; } /** * Convert an OpenLayers vector layer to Cesium primitive collection. * For each feature, the associated primitive will be stored in * `featurePrimitiveMap`. * @api */ olVectorLayerToCesium(olLayer, olView, featurePrimitiveMap) { const proj = olView.getProjection(); const resolution = olView.getResolution(); if (resolution === undefined || !proj) { console.assert(false, 'View not ready'); // an assertion is not enough for closure to assume resolution and proj // are defined throw new Error('View not ready'); } let source = olLayer.getSource(); if (source instanceof OLClusterSource) { source = source.getSource(); } console.assert(source instanceof VectorSource); const features = source.getFeatures(); const counterpart = new VectorLayerCounterpart(proj, this.scene); const context = counterpart.context; for (let i = 0; i < features.length; ++i) { const feature = features[i]; if (!feature) { continue; } const layerStyle = olLayer.getStyleFunction(); const styles = this.computePlainStyle(olLayer, feature, layerStyle, resolution); if (!styles || !styles.length) { // only 'render' features with a style continue; } let primitives = null; for (let i = 0; i < styles.length; i++) { const prims = this.olFeatureToCesium(olLayer, feature, styles[i], context); if (prims) { if (!primitives) { primitives = prims; } else if (prims) { let i = 0, prim; while ((prim = prims.get(i))) { primitives.add(prim); i++; } } } } if (!primitives) { continue; } featurePrimitiveMap[getUid(feature)] = primitives; counterpart.getRootPrimitive().add(primitives); } return counterpart; } /** * Convert an OpenLayers feature to Cesium primitive collection. * @api */ convert(layer, view, feature, context) { const proj = view.getProjection(); const resolution = view.getResolution(); if (resolution == undefined || !proj) { return null; } /** * @type {ol.StyleFunction|undefined} */ const layerStyle = layer.getStyleFunction(); const styles = this.computePlainStyle(layer, feature, layerStyle, resolution); if (!styles || !styles.length) { // only 'render' features with a style return null; } context.projection = proj; /** * @type {Cesium.Primitive|null} */ let primitives = null; for (let i = 0; i < styles.length; i++) { const prims = this.olFeatureToCesium(layer, feature, styles[i], context); if (!primitives) { primitives = prims; } else if (prims) { let i = 0, prim; while ((prim = prims.get(i))) { primitives.add(prim); i++; } } } return primitives; } } /** * Transform a canvas line dash pattern to a Cesium dash pattern * See https://cesium.com/learn/cesiumjs/ref-doc/PolylineDashMaterialProperty.html#dashPattern * @param lineDash */ export function dashPattern(lineDash) { if (lineDash.length < 2) { lineDash = [1, 1]; } const segments = lineDash.length % 2 === 0 ? lineDash : [...lineDash, ...lineDash]; const total = segments.reduce((a, b) => a + b, 0); const div = total / 16; // create a 16 bit binary string let binaryString = segments.map((segment, index) => { // we alternate between 1 and 0 const digit = index % 2 === 0 ? '1' : '0'; // We scale the segment length to fit 16 slots. let count = Math.round(segment / div); if (index === 0 && count === 0) { // We need to start with a 1 count = 1; } return digit.repeat(count); }).join(''); // We rounded so it might be that the string is too short or too long. // We try to fix it by padding or truncating the string. if (binaryString.length < 16) { binaryString = binaryString.padEnd(16, '0'); } else if (binaryString.length > 16) { binaryString = binaryString.substring(0, 16); } if (binaryString[15] === '1') { // We need to really finish with a 0 binaryString = binaryString.substring(0, 15) + '0'; } console.assert(binaryString.length === 16); return parseInt(binaryString, 2); } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRmVhdHVyZUNvbnZlcnRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9vbGNzL0ZlYXR1cmVDb252ZXJ0ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxXQUFXLE1BQU0sa0JBQWtCLENBQUM7QUFDM0MsT0FBTyxZQUFZLEVBQUUsRUFBd0IsTUFBTSxxQkFBcUIsQ0FBQztBQUN6RSxPQUFPLGVBQWUsTUFBTSxzQkFBc0IsQ0FBQztBQUNuRCxPQUFPLEVBQUMsUUFBUSxJQUFJLHVCQUF1QixFQUFDLE1BQU0sb0JBQW9CLENBQUM7QUFDdkUsT0FBTyxFQUFDLGNBQWMsRUFBRSxTQUFTLEVBQUMsTUFBTSxjQUFjLENBQUM7QUFDdkQsT0FBTyxvQkFBb0IsTUFBTSwyQkFBMkIsQ0FBQztBQUM3RCxPQUFPLEVBQUMsb0JBQW9CLEVBQUUscUJBQXFCLEVBQUUsaUNBQWlDLEVBQUUsbUNBQW1DLEVBQUMsTUFBTSxXQUFXLENBQUM7QUFDOUksT0FBTyxzQkFBc0IsRUFBRSxFQUErQixNQUFNLGtDQUFrQyxDQUFDO0FBQ3ZHLE9BQU8sRUFBQyxNQUFNLEVBQUUsU0FBUyxFQUFDLE1BQU0sV0FBVyxDQUFDO0FBQzVDLE9BQU8sRUFBOFosTUFBTSxRQUFRLENBQUM7QUFLcGIsT0FBTyxFQUEyQyxNQUFNLG1CQUFtQixDQUFDO0FBSTVFLE9BQU8sRUFBQyxRQUFRLElBQUksVUFBVSxFQUE0SSxNQUFNLFlBQVksQ0FBQztBQUU3TCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBbUUvQixNQUFNLENBQUMsT0FBTyxPQUFPLGdCQUFnQjtJQWlCYjtJQWZ0Qjs7T0FFRztJQUNLLG9DQUFvQyxHQUFHLElBQUksQ0FBQyx1QkFBdUIsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFFL0UsMEJBQTBCLEdBQUcsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRSxD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