<?php
/**
 * Copyright (C) 2009-2016 Graham Breach
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * For more information, please contact <graham@goat1000.com>
 */

require_once 'SVGGraph3DGraph.php';

class Bar3DGraph extends ThreeDGraph {

  protected $label_centre = true;
  protected $bx;
  protected $by;
  protected $block_width;

  protected function Draw()
  {
    $body = $this->Grid() . $this->UnderShapes();
    $bar_width = $this->block_width = $this->BarWidth();

    // make the top parallelogram, set it as a symbol for re-use
    list($this->bx, $this->by) = $this->Project(0, 0, $bar_width);
    $top = $this->BarTop();

    $bnum = 0;
    $bspace = max(0, ($this->x_axes[$this->main_x_axis]->Unit() - $bar_width) / 2);
    $this->ColourSetup($this->values->ItemsCount());

    // get the translation for the whole bar
    list($tx, $ty) = $this->Project(0, 0, $bspace);
    $all_group = array();
    if($tx || $ty)
      $all_group['transform'] = "translate($tx,$ty)";

    $bar = array('width' => $bar_width);
    $bars = '';
    $group = array();
    if($this->semantic_classes) {
      $all_group['class'] = 'series';
      $group['class'] = 'series0';
    }
    foreach($this->values[0] as $item) {
      $bar_pos = $this->GridPosition($item, $bnum);

      if($this->legend_show_empty || !is_null($item->value)) {
        $bar_style = array('fill' => $this->GetColour($item, $bnum));
        $this->SetStroke($bar_style, $item);
        $this->SetLegendEntry(0, $bnum, $item, $bar_style);
      }

      if(!is_null($item->value) && !is_null($bar_pos)) {
        $bar['x'] = $bspace + $bar_pos;

        $bar_sections = $this->Bar3D($item, $bar, $top, $bnum);
        if($bar_sections != '') {
          $show_label = $this->AddDataLabel(0, $bnum, $group, $item,
            $bar['x'] + $tx, $bar['y'] + $ty, $bar['width'], $bar['height']);
          $link = $this->GetLink($item, $item->key, $bar_sections);

          $group = array_merge($group, $bar_style);
          if($this->show_tooltips)
            $this->SetTooltip($group, $item, 0, $item->key, $item->value);
          $this->SetStroke($group, $item, 0, 'round');
          $bars .= $this->Element('g', $group, NULL, $link);
          unset($group['id']); // make sure a new one is generated
        }
      }
      ++$bnum;
    }

    if(count($all_group))
      $bars = $this->Element('g', $all_group, NULL, $bars);
    $body .= $bars;
    $body .= $this->OverShapes();
    $body .= $this->Axes();
    return $body;
  }

  /**
   * Returns the width of a bar
   */
  protected function BarWidth()
  {
    if(is_numeric($this->bar_width) && $this->bar_width >= 1)
      return $this->bar_width;
    $unit_w = $this->x_axes[$this->main_x_axis]->Unit();
    $bw = $unit_w - $this->bar_space;
    return max(1, $bw, $this->bar_width_min);
  }

  /**
   * Returns the bar top path details array
   */
  protected function BarTop()
  {
    $bw = $this->block_width;
    $top_id = $this->NewID();
    $g = array('id' => $top_id);
    $bar_top = '';

    if($this->skew_top) {
      $sc = abs($this->by / $bw);
      $a = 90 - $this->project_angle;
      $top = array(
        'd' => "M0,0 l0,-{$bw} l{$bw},0 l0,{$bw} z",
        'transform' => "skewX(-{$a}) scale(1,{$sc})",
        'stroke' => 'none'
      );
      $bar_top = $this->Element('path', $top);
    }
    $top = array('d' => "M0,0 l{$bw},0 l{$this->bx},{$this->by} l-{$bw},0 z");
    if($this->skew_top)
      $top['fill'] = 'none';
    $bar_top .= $this->Element('path', $top);
    $this->defs[] = $this->Element('symbol', NULL, NULL,
      $this->Element('g', $g, NULL, $bar_top));

    return array('xlink:href' => '#' . $top_id);
  }

  /**
   * Returns the SVG code for a 3D bar
   */
  protected function Bar3D($item, &$bar, &$top, $index, $dataset = NULL,
    $start = NULL, $axis = NULL)
  {
    $pos = $this->Bar($item->value, $bar, $start, $axis);
    if(is_null($pos) || $pos > $this->height - $this->pad_bottom)
      return '';

    $side_overlay = min(1, max(0, $this->bar_side_overlay_opacity));
    $top_overlay = min(1, max(0, $this->bar_top_overlay_opacity));
    $front_overlay = min(1, max(0, $this->bar_front_overlay_opacity));

    $bar_side = '';
    $bw = $this->block_width;
    $bh = $bar['height'];
    $side_x = $bar['x'] + $bw;
    if($this->skew_side) {
      $sc = $this->bx / $bw;
      $a = $this->project_angle;
      $side = array(
        'd' => "M0,0 L{$bw},0 l0,{$bh} l-{$bw},0 z",
        'transform' => "translate($side_x,{$bar['y']}) skewY(-{$a}) scale({$sc},1)",
        'stroke' => 'none',
      );
      $bar_side = $this->Element('path', $side);
    }
    $side = array(
      'd' => "M0,0 l{$this->bx},{$this->by} l0,{$bh} l-{$this->bx}," . -$this->by . " z",
      'transform' => "translate($side_x,$bar[y])"
    );
    if($this->skew_side)
      $side['fill'] = 'none';
    if($side_overlay)
      $side['stroke'] = 'none'; // only stroke top layer
    $bar_side .= $this->Element('path', $side);

    if($side_overlay) {
      $side['fill-opacity'] = $side_overlay;
      $side['fill'] = $this->bar_side_overlay_colour;
      unset($side['stroke']);
      $bar_side .= $this->Element('path', $side);
    }

    if(is_null($top)) {
      $bar_top = '';
    } else {
      $top['transform'] = "translate($bar[x],$bar[y])";
      $top['fill'] = $this->GetColour($item, $index, $dataset,
        $this->skew_top ? FALSE : TRUE);
      if($top_overlay)
        $top['stroke'] = 'none';
      $bar_top = $this->Element('use', $top, null, $this->empty_use ? '' : null);

      if($top_overlay) {
        unset($top['stroke']);
        $otop = $top;
        $otop['fill-opacity'] = $top_overlay;
        $otop['fill'] = $this->bar_top_overlay_colour;
        $bar_top .= $this->Element('use', $otop, null, $this->empty_use ? '' : null);
      }
    }

    if($front_overlay)
      $bar['stroke'] = 'none';
    $rect = $this->Element('rect', $bar);

    if($front_overlay) {
      unset($bar['stroke']);
      $obar = $bar;
      $obar['fill-opacity'] = $front_overlay;
      $obar['fill'] = $this->bar_front_overlay_colour;
      $rect .= $this->Element('rect', $obar);
    }

    return $rect . $bar_top . $bar_side;
  }

  /**
   * Fills in the y-position and height of a bar (copied from BarGraph)
   * @param number $value value
   * @param array  &$bar  element array [out]
   * @param number $start start value
   * @param number $axis axis number
   * @return number unclamped bar position
   */
  protected function Bar($value, &$bar, $start = null, $axis = NULL)
  {
    if($start)
      $value += $start;

    $startpos = is_null($start) ? $this->OriginY($axis) :
      $this->GridY($start, $axis);
    $pos = $this->GridY($value, $axis);
    if(is_null($pos)) {
      $bar['height'] = 0;
    } else {
      $l1 = $this->ClampVertical($startpos);
      $l2 = $this->ClampVertical($pos);
      $bar['y'] = min($l1, $l2);
      $bar['height'] = abs($l1-$l2);
    }
    return $pos;
  }

  /**
   * Override to check minimum space requirement
   */
  protected function AddDataLabel($dataset, $index, &$element, &$item,
    $x, $y, $w, $h, $content = NULL, $duplicate = TRUE)
  {
    if($h < $this->ArrayOption($this->data_label_min_space, $dataset))
      return false;
    return parent::AddDataLabel($dataset, $index, $element, $item, $x, $y,
      $w, $h, $content, $duplicate);
  }

  /**
   * Return box for legend
   */
  public function DrawLegendEntry($x, $y, $w, $h, $entry)
  {
    $bar = array('x' => $x, 'y' => $y, 'width' => $w, 'height' => $h);
    return $this->Element('rect', $bar, $entry->style);
  }
}