Source: symbols/custom/diagramSymbol.js

let ol = require('openlayers');

/**
 * Устанавливает символ диаграммы для векторного объекта
 * @constructor
 * @param {object} params - параметрмы диаграммы
 *  @param {String} options.type - тип диаграммы - доступные значения: pie,pie3D, donut, bar (пока не закончен)
 *  @param {number} options.radius - радиус диаграммы - по умолчанию 20
 *  @param {number} options.rotation - угол поворота в радианах - 0
 *  @param {bool} options.snapToPixel - привязка к пикселам  - true
 *  @param {object} - параметры стиля обводки
 *  @param {number} options.offsetX - отступ по горизонтали (px)
 *  @param {number} options.offsetY - отступ по вертикали (px)
 * @extends {ol.style.RegularShape}
 * @implements {ol.structs.IHasChecksum}
 * @api
 */

let DiagramSymbol = function (params, feature) {
  let options = params || {};

  let strokeWidth = 0;
  if (params.stroke) strokeWidth = params.stroke.getWidth();
  ol.style.RegularShape.call(this,
    {
      radius: options.radius + strokeWidth,
      fill: new ol.style.Fill({color: [0, 0, 0]}),
      rotation: options.rotation,
      snapToPixel: options.snapToPixel
    });
  if (options.scale) this.setScale(options.scale);

  //this.stroke_ = params.default.stroke ? new ol.style.Stroke(params.default.stroke) : new ol.style.Stroke({color: "#FFF", width: 1});
  this.radius_ = options.radius || 20;
  this.donutRatio_ = options.donutRatio || 0.5;
  this.type_ = options.type;
  this.offset_ = [options.offsetX ? options.offsetX : 0, options.offsetY ? options.offsetY : 0];
  this.animation_ = (typeof(options.animation) == 'number') ? {animate: true, step: options.animation} : this.animation_ = {animate: false, step: 1};

  let properties = feature.getProperties();
  let data = [];
  let colors = [];

  for (let key in options.values) {
    let value = properties[key] || 0;
    let color = options.values[key].fill.color || options.default.fill.color;
    data.push(value);
    colors.push(color);
  }

  this.data_ = data || options.data;
  this.colors_ = colors;

  this.renderChart_();
};
ol.inherits(DiagramSymbol, ol.style.RegularShape);

/**
 * Копия символа (необходима для ускорения создания)
 * @return {DiagramSymbol}
 */
DiagramSymbol.prototype.clone = function () {
  let symbol = new DiagramSymbol(
    {
      type: this.type_,
      radius: this.radius_,
      rotation: this.getRotation(),
      scale: this.getScale(),
      data: this.getData(),
      snapToPixel: this.getSnapToPixel(),
      stroke: this.stroke_,
      colors: this.colors_,
      offsetX: this.offset_[0],
      offsetY: this.offset_[1],
      animation: this.animation_
    });
  symbol.setScale(this.getScale());
  symbol.setOpacity(this.getOpacity());
  return symbol;
};

DiagramSymbol.prototype.getData = function () {
  return this.data_;
};

/**
 * Принудительное обновление диаграммы
 * @param data
 */
DiagramSymbol.prototype.setData = function (data) {
  this.data_ = data;
  this.renderChart_();
};

/**
 * Получение радиуса
 */
DiagramSymbol.prototype.getRadius = function () {
  return this.radius_;
};

/**
 * Установка радиуса
 *  @param {number} radius - радиус
 *  @param {number} ratio - коэффициент сжатия
 */
DiagramSymbol.prototype.setRadius = function (radius, ratio) {
  this.radius_ = radius;
  this.donuratio_ = ratio || this.donuratio_;
  this.renderChart_();
};

/**
 * Включение анимации
 *  @param {false|number}
 */
DiagramSymbol.prototype.setAnimation = function (step) {
  if (step === false) {
    if (this.animation_.animate == false) return;
    this.animation_.animate = false;
  }
  else {
    if (this.animation_.step == step) return;
    this.animation_.animate = true;
    this.animation_.step = step;
  }
  this.renderChart_();
};

/**
 * Отрисовка диаграммы
 */
DiagramSymbol.prototype.renderChart_ = function (atlasManager) {
  let strokeStyle;
  let strokeWidth = 0;

  if (this.stroke_) {
    strokeStyle = ol.Color.asString(this.stroke_.getColor());
    strokeWidth = this.stroke_.getWidth();
  }

  let canvas = this.getImage();

  let context = (canvas.getContext('2d'));
  context.clearRect(0, 0, canvas.width, canvas.height);

  context.lineJoin = "round";

  let sum = this.data_.reduce((accumulator, value) => {
    return accumulator + value
  });

  context.setTransform(1, 0, 0, 1, 0, 0);
  context.translate(0, 0);

  let step = this.animation_.animate ? this.animation_.step : 1;
  let a, a0 = Math.PI * (step - 1.5);
  let c = canvas.width / 2;

  switch (this.type_) {
    // Круговые диаграммы
    case "donut":
    case "pie3D":
    case "pie": {
      context.strokeStyle = strokeStyle;
      context.lineWidth = strokeWidth;
      context.save();
      if (this.type_ === "pie3D") {
        context.translate(0, c * 0.3);
        context.scale(1, 0.7);
        context.beginPath();
        context.fillStyle = "#369";
        context.arc(c, c * 1.4, this.radius_ * step, 0, 2 * Math.PI);
        context.fill();
        context.stroke();
      }
      if (this.type_ === "donut") {
        context.save();
        context.beginPath();
        context.rect(0, 0, 2 * c, 2 * c);
        context.arc(c, c, this.radius_ * step * this.donutRatio_, 0, 2 * Math.PI);
        context.clip("evenodd");
      }
      for (let i = 0; i < this.data_.length; i++) {
        context.beginPath();
        context.moveTo(c, c);
        context.fillStyle = this.colors_[i % this.colors_.length];
        a = a0 + 2 * Math.PI * this.data_[i] / sum * step;
        context.arc(c, c, this.radius_ * step, a0, a);
        context.closePath();
        context.fill();
        context.stroke();
        a0 = a;
      }
      if (this.type_ === "donut") {
        context.restore();
        context.beginPath();
        context.strokeStyle = strokeStyle;
        context.lineWidth = strokeWidth;
        context.arc(c, c, this.radius_ * step * this.donutRatio_, Math.PI * (step - 1.5), a0);
        context.stroke();
      }
      context.restore();
      break;
    }
    // столбчатая диаграмма
    case "bar":
    default: {
      let max = 0;
      for (let i = 0; i < this.data_.length; i++) {
        if (max < this.data_[i]) max = this.data_[i];
      }
      let s = Math.min(5, 2 * this.radius_ / this.data_.length);
      let c = canvas.width / 2;
      let b = canvas.width - strokeWidth;
      let x, x0 = c - this.data_.length * s / 2
      context.strokeStyle = strokeStyle;
      context.lineWidth = strokeWidth;
      for (let i = 0; i < this.data_.length; i++) {
        context.beginPath();
        context.fillStyle = this.colors_[i % this.colors_.length];
        x = x0 + s;
        let h = this.data_[i] / max * 2 * this.radius_ * step;
        context.rect(x0, b - h, s, h);
        context.closePath();
        context.fill();
        context.stroke();
        x0 = x;
      }

    }
  }

  // установка привязки (смещения)
  let anchor = this.getAnchor();
  anchor[0] = c - this.offset_[0];
  anchor[1] = c - this.offset_[1];
};

/**
 * Переопределяемый метод - возвращает контрольную сумму символа
 */
DiagramSymbol.prototype.getChecksum = function () {
  let strokeChecksum = (this.stroke_ !== null) ?
    this.stroke_.getChecksum() : '-';

  let recalculate = (this.checksums_ === null) ||
    (strokeChecksum != this.checksums_[1] ||
      fillChecksum != this.checksums_[2] ||
      this.radius_ != this.checksums_[3] ||
      this.data_.join('|') != this.checksums_[4]);

  if (recalculate) {
    let checksum = 'c' + strokeChecksum + fillChecksum
      + ((this.radius_ !== void 0) ? this.radius_.toString() : '-')
      + this.data_.join('|');
    this.checksums_ = [checksum, strokeChecksum, fillChecksum, this.radius_, this.data_.join('|')];
  }

  return this.checksums_[0];
};

module.exports = DiagramSymbol;