Source: FeatureConverter.js

/**
 * @module
 */
import olLayerImage from 'ol/layer/Image';
import olGeomGeometry from 'ol/geom/Geometry';
import olSourceImageVector from 'ol/source/ImageVector';
import olStyleIcon from 'ol/style/Icon';
import olSourceVector from 'ol/source/Vector';
import olSourceCluster from 'ol/source/Cluster';
import googAsserts from 'goog/asserts';
import olBase from 'ol';
import olEvents from 'ol/events';
import olExtent from 'ol/extent';
import olGeomSimpleGeometry from 'ol/geom/SimpleGeometry';
import olcsCore from './core.js';
import olcsCoreVectorLayerCounterpart from './core/VectorLayerCounterpart.js';
import olcsUtil from './util.js';

const exports = function(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);
};


/**
 * @param {ol.source.Vector.Event} evt
 * @private
 */
exports.prototype.onRemoveOrClearFeature_ = function(evt) {
  const source = evt.target;
  googAsserts.assertInstanceof(source, olSourceVector);

  const cancellers = olcsUtil.obj(source)['olcs_cancellers'];
  if (cancellers) {
    const feature = evt.feature;
    if (feature) {
      // remove
      const id = olBase.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
 */
exports.prototype.setReferenceForPicking = function(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
 */
exports.prototype.createColoredPrimitive = function(layer, feature, olGeometry, geometry, color, opt_lineWidth) {
  const createInstance = function(geometry, color) {
    return new Cesium.GeometryInstance({
      // always update Cesium externs before adding a property
      geometry,
      attributes: {
        color: Cesium.ColorGeometryInstanceAttribute.fromColor(color)
      }
    });
  };

  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({
      // always update Cesium externs before adding a property
      geometryInstances: instances
    });
  } else {
    const appearance = new Cesium.PerInstanceColorAppearance(options);
    primitive = new Cesium.Primitive({
      // always update Cesium externs before adding a property
      geometryInstances: instances,
      appearance
    });
  }

  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
 */
exports.prototype.extractColorFromOlStyle = function(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
 */
exports.prototype.extractLineWidthFromOlStyle = function(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
 */
exports.prototype.wrapFillAndOutlineGeometries = function(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);
    googAsserts.assert(!!p1);
    primitives.add(p1);
  }

  if (olStyle.getStroke()) {
    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
 */
exports.prototype.addTextStyle = function(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
 */
exports.prototype.csAddBillboard = function(billboards, bbOptions, layer, feature, geometry, style) {
  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
 */
exports.prototype.olCircleGeometryToCesium = function(layer, feature, olGeometry, projection, olStyle) {

  olGeometry = olcsCore.olGeometryCloneTo4326(olGeometry, projection);
  googAsserts.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
  });

  const 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);

  return this.addTextStyle(layer, feature, olGeometry, olStyle, primitives);
};


/**
 * 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
 */
exports.prototype.olLineStringGeometryToCesium = function(layer, feature, olGeometry, projection, olStyle) {

  olGeometry = olcsCore.olGeometryCloneTo4326(olGeometry, projection);
  googAsserts.assert(olGeometry.getType() == 'LineString');

  const positions = olcsCore.ol4326CoordinateArrayToCsCartesians(
      olGeometry.getCoordinates());

  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: this.extractLineWidthFromOlStyle(olStyle),
    vertexFormat: appearance.vertexFormat
  };

  let outlinePrimitive;
  const heightReference = this.getHeightReference(layer, feature, olGeometry);

  if (heightReference == Cesium.HeightReference.CLAMP_TO_GROUND) {
    const color = this.extractColorFromOlStyle(olStyle, true);
    outlinePrimitive = new Cesium.GroundPrimitive({
      // always update Cesium externs before adding a property
      geometryInstances: new Cesium.GeometryInstance({
        geometry: new Cesium.CorridorGeometry(geometryOptions),
        attributes: {
          color: Cesium.ColorGeometryInstanceAttribute.fromColor(color)
        }
      })
    });
  } else {
    outlinePrimitive = new Cesium.Primitive({
      // always update Cesium externs before adding a property
      geometryInstances: new Cesium.GeometryInstance({
        geometry: new Cesium.PolylineGeometry(geometryOptions)
      }),
      appearance
    });
  }

  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
 */
exports.prototype.olPolygonGeometryToCesium = function(layer, feature, olGeometry, projection, olStyle) {

  olGeometry = olcsCore.olGeometryCloneTo4326(olGeometry, projection);
  googAsserts.assert(olGeometry.getType() == 'Polygon');

  let fillGeometry, outlineGeometry;
  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 = olExtent.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;
    googAsserts.assert(rings.length > 0);

    for (let i = 0; i < rings.length; ++i) {
      const olPos = rings[i].getCoordinates();
      const positions = olcsCore.ol4326CoordinateArrayToCsCartesians(olPos);
      googAsserts.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
    });

    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);

  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
 */
exports.prototype.getHeightReference = function(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 {!Cesium.BillboardCollection} billboards
 * @param {function(!Cesium.Billboard)=} opt_newBillboardCallback Called when
 * the new billboard is added.
 * @return {Cesium.Primitive} primitives
 * @api
 */
exports.prototype.olPointGeometryToCesium = function(layer, feature, olGeometry, projection, style, billboards,
    opt_newBillboardCallback) {
  googAsserts.assert(olGeometry.getType() == 'Point');
  olGeometry = olcsCore.olGeometryCloneTo4326(olGeometry, projection);

  const imageStyle = style.getImage();
  if (imageStyle) {
    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 heightReference = this.getHeightReference(layer, feature, olGeometry);

      const bbOptions = /** @type {Cesium.optionsBillboardCollectionAdd} */ ({
        // always update Cesium externs before adding a property
        image,
        color,
        scale: imageStyle.getScale(),
        heightReference,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        position
      });
      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;
      let source = layer.getSource();
      if (source instanceof olSourceImageVector) {
        source = source.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 = olBase.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() {
        if (!billboards.isDestroyed() && !cancelled) {
          // Create billboard if the feature is still displayed on the map.
          reallyCreateBillboard();
        }
      };

      olEvents.listenOnce(image, 'load', listener);
    } else {
      reallyCreateBillboard();
    }
  }

  if (style.getText()) {
    return this.addTextStyle(layer, feature, olGeometry, style,
        new Cesium.Primitive());
  } else {
    return null;
  }
};


/**
 * 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
 */
exports.prototype.olMultiGeometryToCesium = function(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) => {
          googAsserts.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) => {
          googAsserts.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:
      googAsserts.fail(`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
 */
exports.prototype.olGeometry4326TextPartToCesium = function(layer, feature, geometry, style) {
  const text = style.getText();
  googAsserts.assert(text !== undefined);


  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 = olExtent.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;
  }

  const font = style.getFont();
  if (font !== undefined) {
    options.font = font;
  }

  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:
        googAsserts.fail(`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
 */
exports.prototype.olStyleToCesium = function(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.<!ol.style.Style>} null if no style is available
 * @api
 */
exports.prototype.computePlainStyle = function(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.call(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}
 */
exports.prototype.getGeometryFromFeature = function(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 {!olcsx.core.OlFeatureToCesiumContext} context
 * @param {!ol.geom.Geometry=} opt_geom Geometry to be converted.
 * @return {Cesium.Primitive} primitives
 * @api
 */
exports.prototype.olFeatureToCesium = function(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) {
    context.featureToCesiumMap[olBase.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.<number, !Cesium.Primitive>} featurePrimitiveMap
 * @return {!olcs.core.VectorLayerCounterpart}
 * @api
 */
exports.prototype.olVectorLayerToCesium = function(olLayer, olView, featurePrimitiveMap) {
  const proj = olView.getProjection();
  const resolution = olView.getResolution();

  if (resolution === undefined || !proj) {
    googAsserts.fail('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 (olLayer instanceof olLayerImage) {
    if (source instanceof olSourceImageVector) {
      source = source.getSource();
    } else {
      // Not supported
      return new olcsCoreVectorLayerCounterpart(proj, this.scene);
    }
  }
  if (source instanceof olSourceCluster) {
    source = source.getSource();
  }

  googAsserts.assertInstanceof(source, 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}
     */
    let layerStyle;
    if (olLayer instanceof olLayerImage) {
      const imageSource = olLayer.getSource();
      googAsserts.assertInstanceof(imageSource, olSourceImageVector);
      layerStyle = imageSource.getStyleFunction();
    } else {
      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[olBase.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 {!olcsx.core.OlFeatureToCesiumContext} context
 * @return {Cesium.Primitive}
 * @api
 */
exports.prototype.convert = function(layer, view, feature, context) {
  const proj = view.getProjection();
  const resolution = view.getResolution();

  if (resolution == undefined || !proj) {
    return null;
  }

  /**
   * @type {ol.StyleFunction|undefined}
   */
  let layerStyle;
  if (layer instanceof olLayerImage) {
    const imageSource = layer.getSource();
    if (imageSource instanceof olSourceImageVector) {
      layerStyle = imageSource.getStyleFunction();
    } else {
      return null;
    }
  } else {
    layerStyle = layer.getStyleFunction();
  }

  const styles = this.computePlainStyle(layer, feature, layerStyle, resolution);

  if (!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 exports;