<?php /** * Copyright (C) 2012-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 'SVGGraphLineGraph.php'; /** * RadarGraph - a line graph that goes around in circles */ class RadarGraph extends LineGraph { protected $xc; protected $yc; protected $radius; protected $arad; private $pad_v_axis_label; // in the case of radar graphs, $label_centre means we want an axis that // ends at N points + 1 protected $label_centre = true; protected $require_integer_keys = false; protected $single_axis = true; protected function Draw() { $body = $this->Grid() . $this->UnderShapes(); $attr = array('stroke' => $this->stroke_colour, 'fill' => 'none'); $dash = is_array($this->line_dash) ? $this->line_dash[0] : $this->line_dash; $stroke_width = is_array($this->line_stroke_width) ? $this->line_stroke_width[0] : $this->line_stroke_width; if(!is_null($dash)) $attr['stroke-dasharray'] = $dash; $attr['stroke-width'] = $stroke_width <= 0 ? 1 : $stroke_width; $this->ColourSetup($this->values->ItemsCount()); $bnum = 0; $cmd = 'M'; $path = ''; if($this->fill_under) { $attr['fill'] = $this->GetColour(null, 0); $this->curr_fill_style = array( 'fill' => $attr['fill'], 'stroke' => $attr['fill'] ); if($this->fill_opacity < 1.0) { $attr['fill-opacity'] = $this->fill_opacity; $this->curr_fill_style['fill-opacity'] = $this->fill_opacity; } } $y_axis = $this->y_axes[$this->main_y_axis]; $marker_points = array(); foreach($this->values[0] as $item) { $point_pos = $this->GridPosition($item, $bnum); if(!is_null($item->value) && !is_null($point_pos)) { $val = $y_axis->Position($item->value); if(!is_null($val)) { $angle = $this->arad + $point_pos / $this->g_height; $x = $this->xc + ($val * sin($angle)); $y = $this->yc + ($val * cos($angle)); $path .= "$cmd$x $y "; // no need to repeat same L command $cmd = $cmd == 'M' ? 'L' : ''; $marker_points[$bnum] = compact('x', 'y', 'item'); } } ++$bnum; } $path .= "z"; $this->curr_line_style = $attr; foreach($marker_points as $bnum => $m) { $marker_id = $this->MarkerLabel(0, $bnum, $m['item'], $m['x'], $m['y']); $extra = empty($marker_id) ? NULL : array('id' => $marker_id); $this->AddMarker($m['x'], $m['y'], $m['item'], $extra); } $attr['d'] = $path; $group = array(); $this->ClipGrid($group); if($this->semantic_classes) { $group['class'] = 'series'; $attr['class'] = "series0"; } $body .= $this->Element('g', $group, NULL, $this->Element('path', $attr)); $body .= $this->OverShapes(); $body .= $this->Axes(); $body .= $this->CrossHairs(); $body .= $this->DrawMarkers(); return $body; } /** * Finds the grid position for radar graphs, returns NULL if not on graph */ protected function GridPosition($item, $ikey) { $gkey = $this->values->AssociativeKeys() ? $ikey : $item->key; $axis = $this->x_axes[$this->main_x_axis]; $offset = $axis->Position($gkey); if($offset >= 0 && $offset < $this->g_width) return $this->reverse ? -$offset : $offset; return NULL; } /** * Returns the $x value as a grid position */ public function GridX($x, $axis_no = NULL) { $p = $this->UnitsX($x, $axis_no); return $p; } /** * Returns the $y value as a grid position */ public function GridY($y, $axis_no = NULL) { $p = $this->UnitsY($y, $axis_no); return $p; } /** * Convert X, Y in grid space to radial position */ public function TransformCoords($x, $y) { $angle = $x / $this->g_height; if($this->grid_straight) { // this complicates things... // get all the grid points, div and subdiv $points = array_merge($this->GetGridPointsX(0), $this->GetSubDivsX(0)); $grid_angles = array(); foreach($points as $point) { $grid_angles[] = $point->position / $this->radius; } sort($grid_angles); // find angle between (sub)divisions $div_angle = $grid_angles[1] - $grid_angles[0]; // use trig to find length of Y $a = fmod($angle, $div_angle); $t = ($div_angle / 2) - $a; $y2 = $y * cos($div_angle / 2); $y = $y2 / cos($t); } $angle += $this->arad; $new_x = $this->xc + ($y * sin($angle)); $new_y = $this->yc + ($y * cos($angle)); return array($new_x, $new_y); } /** * Find the bounding box of the axis text for given axis lengths */ protected function FindAxisTextBBox($length_x, $length_y, $x_axes, $y_axes) { $this->xc = $length_x / 2; $this->yc = $length_y / 2; $diameter = min($length_x, $length_y); $length_y = $diameter / 2; $length_x = 2 * M_PI * $length_y; $this->radius = $length_y; foreach($x_axes as $a) $a->SetLength($length_x); foreach($y_axes as $a) $a->SetLength($length_y); $min_space_h = $this->GetFirst($this->minimum_grid_spacing_h, $this->minimum_grid_spacing); // Code from parent implementation, with minor changes // initialise maxima and minima $min_x = $this->width; $min_y = $this->height; $max_x = $max_y = 0; // need actual text positions $div_size = $this->DivisionOverlap($x_axes, $y_axes); $inside_x = ('inside' == $this->GetFirst($this->axis_text_position_h, $this->axis_text_position)); $font_size = $this->axis_font_size; // if outside, use the division overlap as starting positions $min_x = - $div_size['l']; $max_y = $length_y + $div_size['b']; // only do this if there is x-axis text if($this->show_axis_text_h) { $x_axis = $x_axes[0]; $offset = 0; $points = $x_axis->GetGridPoints(0); $positions = $this->XAxisTextPositions($points, $offset, $div_size['b'], $this->axis_text_angle_h, $inside_x); foreach($positions as $p) { switch($p['text-anchor']) { case 'middle' : $off_x = $p['w'] / 2; break; case 'end' : $off_x = $p['w']; break; default : $off_x = 0; } $x = $p['x'] - $off_x; $y = $p['y'] - $font_size; $xw = $x + $p['w']; $yh = $y + $p['h']; if($x < $min_x) $min_x = $x; if($xw > $max_x) $max_x = $xw; if($y < $min_y) $min_y = $y; if($yh > $max_y) $max_y = $yh; } } if($this->show_axis_text_v) { $axis_no = -1; foreach($y_axes as $y_axis) { ++$axis_no; if(is_null($y_axis)) continue; $offset = 0; $inside_y = ('inside' == $this->GetFirst( $this->ArrayOption($this->axis_text_position_v, $axis_no), $this->axis_text_position)); $points = $y_axis->GetGridPoints(0); $positions = $this->YAxisTextPositions($points, $div_size['l'], $offset, $this->ArrayOption($this->axis_text_angle_v, $axis_no), false, $axis_no); foreach($positions as $p) { $x = $p['x'];// - ($p['text-anchor'] == 'end' ? $p['w'] : 0); $y = $p['y'];// - $font_size + $length_y; // this messes up Radar graphs padding $xw = $x + $p['w']; $yh = $y + $p['h']; if($x < $min_x) $min_x = $x; if($xw > $max_x) $max_x = $xw; if($y < $min_y) $min_y = $y; if($yh > $max_y) $max_y = $yh; } } } // end of GridGraph implementation code // normalise the bounding box $w_half = ($max_x - $min_x) / 2; $h_half = ($max_y - $min_y) / 2; $bbox = array( 'min_x' => $this->xc - $w_half, 'max_x' => $this->xc + $w_half, 'min_y' => $this->yc - $h_half, 'max_y' => $this->yc + $h_half ); $this->radius = null; return $bbox; } /** * Draws concentric Y grid lines */ protected function YGrid(&$y_points) { $path = ''; if($this->grid_straight) { $grid_angles = array(); $points = array_merge($this->GetGridPointsX(0), $this->GetSubDivsX(0)); foreach($points as $point) { $new_x = $point->position - $this->pad_left; $grid_angles[] = $this->arad + $new_x / $this->radius; } // put the grid angles in order sort($grid_angles); foreach($y_points as $y) { $y = $y->position; $x1 = $this->xc + $y * sin($this->arad); $y1 = $this->yc + $y * cos($this->arad); $path .= "M$x1 {$y1}L"; foreach($grid_angles as $a) { $x1 = $this->xc + $y * sin($a); $y1 = $this->yc + $y * cos($a); $path .= "$x1 $y1 "; } $path .= "z"; } } else { foreach($y_points as $y) { $y = $y->position; $p1 = $this->xc - $y; $p2 = $this->xc + $y; $path .= "M$p1 {$this->yc}A $y $y 0 1 1 $p2 {$this->yc}"; $path .= "M$p2 {$this->yc}A $y $y 0 1 1 $p1 {$this->yc}"; } } return $path; } /** * Draws radiating X grid lines */ protected function XGrid(&$x_points) { $path = ''; foreach($x_points as $x) { $x = $x->position - $this->pad_left; $angle = $this->arad + $x / $this->radius; $p1 = $this->radius * sin($angle); $p2 = $this->radius * cos($angle); $path .= "M{$this->xc} {$this->yc}l$p1 $p2"; } return $path; } /** * Draws the grid behind the graph */ protected function Grid() { $this->CalcAxes(); $this->CalcGrid(); if(!$this->show_grid || (!$this->show_grid_h && !$this->show_grid_v)) return ''; $xc = $this->xc; $yc = $this->yc; $r = $this->radius; $back = $subpath = ''; $back_colour = $this->ParseColour($this->grid_back_colour, null, false, false, true); $y_points = $this->GetGridPointsY(0); $x_points = $this->GetGridPointsX(0); $y_subdivs = $this->GetSubDivsY(0); $x_subdivs = $this->GetSubDivsX(0); if(!empty($back_colour) && $back_colour != 'none') { // use the YGrid function to get the path $points = array(new GridPoint($r, '', 0)); $bpath = array( 'd' => $this->YGrid($points), 'fill' => $back_colour ); if($this->grid_back_opacity != 1) $bpath['fill-opacity'] = $this->grid_back_opacity; $back = $this->Element('path', $bpath); } if($this->grid_back_stripe) { // use array of colours if available, otherwise stripe a single colour $colours = is_array($this->grid_back_stripe_colour) ? $this->grid_back_stripe_colour : array(NULL, $this->grid_back_stripe_colour); $c = 0; $num_colours = count($colours); $num_points = count($y_points); while($c < $num_points - 1) { if(!is_null($colours[$c % $num_colours])) { $s_points = array($y_points[$c], $y_points[$c + 1]); $bpath = array( 'fill' => $this->ParseColour($colours[$c % $num_colours]), 'd' => $this->YGrid($s_points), 'fill-rule' => 'evenodd', ); if($this->grid_back_stripe_opacity != 1) $bpath['fill-opacity'] = $this->grid_back_stripe_opacity; $back .= $this->Element('path', $bpath); } ++$c; } } if($this->show_grid_subdivisions) { $subpath_h = $this->show_grid_h ? $this->YGrid($y_subdivs) : ''; $subpath_v = $this->show_grid_v ? $this->XGrid($x_subdivs) : ''; if($subpath_h != '' || $subpath_v != '') { $colour_h = $this->GetFirst($this->grid_subdivision_colour_h, $this->grid_subdivision_colour, $this->grid_colour_h, $this->grid_colour); $colour_v = $this->GetFirst($this->grid_subdivision_colour_v, $this->grid_subdivision_colour, $this->grid_colour_v, $this->grid_colour); $dash_h = $this->GetFirst($this->grid_subdivision_dash_h, $this->grid_subdivision_dash, $this->grid_dash_h, $this->grid_dash); $dash_v = $this->GetFirst($this->grid_subdivision_dash_v, $this->grid_subdivision_dash, $this->grid_dash_v, $this->grid_dash); if($dash_h == $dash_v && $colour_h == $colour_v) { $subpath = $this->GridLines($subpath_h . $subpath_v, $colour_h, $dash_h, 'none'); } else { $subpath = $this->GridLines($subpath_h, $colour_h, $dash_h, 'none') . $this->GridLines($subpath_v, $colour_v, $dash_v, 'none'); } } } $path_v = $this->show_grid_h ? $this->YGrid($y_points) : ''; $path_h = $this->show_grid_v ? $this->XGrid($x_points) : ''; $colour_h = $this->GetFirst($this->grid_colour_h, $this->grid_colour); $colour_v = $this->GetFirst($this->grid_colour_v, $this->grid_colour); $dash_h = $this->GetFirst($this->grid_dash_h, $this->grid_dash); $dash_v = $this->GetFirst($this->grid_dash_v, $this->grid_dash); if($dash_h == $dash_v && $colour_h == $colour_v) { $path = $this->GridLines($path_v . $path_h, $colour_h, $dash_h, 'none'); } else { $path = $this->GridLines($path_h, $colour_h, $dash_h, 'none') . $this->GridLines($path_v, $colour_v, $dash_v, 'none'); } return $back . $subpath . $path; } /** * Sets the grid size as circumference x radius */ protected function SetGridDimensions() { if(is_null($this->radius)) { $w = $this->width - $this->pad_left - $this->pad_right; $h = $this->height - $this->pad_top - $this->pad_bottom; $this->xc = $this->pad_left + $w / 2; $this->yc = $this->pad_top + $h / 2; $this->radius = min($w, $h) / 2; } $this->g_height = $this->radius; $this->g_width = 2 * M_PI * $this->radius; } /** * Calculate the extra details for radar axes */ protected function CalcAxes($h_by_count = false, $bar = false) { $this->arad = (90 + $this->start_angle) * M_PI / 180; $this->axis_right = false; parent::CalcAxes($h_by_count, $bar); } /** * The X-axis is wrapped around the graph */ protected function XAxis($yoff) { if(!$this->show_x_axis) return ''; // use the YGrid function to get the path $points = array(new GridPoint($this->radius, '', 0)); $path = array( 'd' => $this->YGrid($points), 'fill' => 'none' // it's a circle or polygon, don't want it filled ); if(!empty($this->axis_colour_h)) $path['stroke'] = $this->axis_colour_h; if(!empty($this->axis_stroke_width_h)) $path['stroke-width'] = $this->axis_stroke_width_h; return $this->Element('path', $path); } /** * The Y-axis is at start angle */ protected function YAxis($i) { $radius = $this->radius + $this->axis_overlap; $x1 = $radius * sin($this->arad); $y1 = $radius * cos($this->arad); $path = array('d' => "M{$this->xc} {$this->yc}l$x1 $y1"); $colour = $this->ArrayOption($this->axis_colour_v, $i); $thickness = $this->ArrayOption($this->axis_stroke_width_v, $i); if(!empty($colour)) $path['stroke'] = $colour; if(!empty($thickness)) $path['stroke-width'] = $thickness; return $this->Element('path', $path); } /** * Division marks around the graph */ protected function XAxisDivisions(&$points, $style, $size, $yoff) { $r1 = $this->radius; $path = ''; $pos = $this->DivisionsPositions($style, $size, $this->radius, 0, 0, false, false); if(is_null($pos)) return ''; $r1 = $this->radius - $pos['pos']; foreach($points as $p) { $p = $p->position - $this->pad_left; $a = $this->arad + $p / $this->radius; $x1 = $this->xc + $r1 * sin($a); $y1 = $this->yc + $r1 * cos($a); $x2 = -$pos['sz'] * sin($a); $y2 = -$pos['sz'] * cos($a); $path .= "M$x1 {$y1}l$x2 $y2"; } return $path; } /** * Draws Y-axis divisions at whatever angle the Y-axis is */ protected function YAxisDivisions(&$points, $xoff, $subdiv, $axis_no) { $dz = 'division_size'; $ds = 'division_style'; $dzv = 'division_size_v'; $dsv = 'division_style_v'; if($subdiv) { $dz = 'subdivision_size'; $ds = 'subdivision_style'; $dzv = 'subdivision_size_v'; $dsv = 'subdivision_style_v'; } $style = $this->GetFirst($this->ArrayOption($this->{$dsv}, $axis_no), $this->{$ds}); $size = $this->GetFirst($this->ArrayOption($this->{$dzv}, $axis_no), $this->{$dz}); $path = ''; $pos = $this->DivisionsPositions($style, $size, $size, 0, 0, false, false); if(is_null($pos)) return ''; $a = $this->arad + ($this->arad <= M_PI_2 ? - M_PI_2 : M_PI_2); $px = $pos['pos'] * sin($a); $py = $pos['pos'] * cos($a); $x2 = $pos['sz'] * sin($a); $y2 = $pos['sz'] * cos($a); $c = cos($this->arad); $s = sin($this->arad); foreach($points as $y) { $y = $y->position; $x1 = ($this->xc + $y * $s) + $px; $y1 = ($this->yc + $y * $c) + $py; $path .= "M$x1 {$y1}l$x2 $y2"; } return $path; } /** * Returns the positions of the X-axis text */ protected function XAxisTextPositions(&$points, $xoff, $yoff, $angle, $inside) { $positions = array(); $font_size = $this->GetFirst( $this->ArrayOption($this->axis_font_size_h, 0), $this->axis_font_size); $font_adjust = $this->GetFirst( $this->ArrayOption($this->axis_font_adjust_h, 0), $this->axis_font_adjust); $text_space = $this->GetFirst( $this->ArrayOption($this->axis_text_space_h, 0), $this->axis_text_space); $r = $this->radius + $yoff + $text_space; $text_centre = $font_size * 0.3; $count = count($points); $p = 0; $direction = $this->reverse ? -1 : 1; foreach($points as $grid_point) { $key = $grid_point->text; $x = $grid_point->position - $this->pad_left; if(SVGGraphStrlen($key, $this->encoding) > 0 && ++$p < $count) { $a = $this->arad + $direction * $x / $this->radius; $s = sin($a); $c = cos($a); $x1 = $r * $s; $y1 = $r * $c - $text_centre; $position = array( 'x' => $this->xc + $x1, 'y' => $this->yc + $y1, // $c == +1 or -1 is a particular case: anchor on middle of text 'text-anchor' => (pow($c, 2) == 1 ? 'middle' : ($x1 >= 0 ? 'start' : 'end')), 'angle' => $a, 'sin' => $s, 'cos' => $c ); $size = $this->TextSize((string)$key, $font_size, $font_adjust, $this->encoding, $angle, $font_size); // $s == +1 or -1 is a particular case: vertically centre $lines = $this->CountLines($key); if(pow($s, 2) == 1) $position['y'] -= ($lines / 2 - 1) * $font_size; elseif($c < 0) $position['y'] -= ($lines - 1) * $font_size; else $position['y'] += $font_size; if($angle != 0) { $rcx = $position['x']; $rcy = $position['y']; if($c < 0) $rcy += $font_size; elseif(pow($s, 2) != 1) $rcy -= $font_size; $position['transform'] = "rotate($angle,$rcx,$rcy)"; } // $c == -1 is particular too : XAxis text can bump YAxis texts $y_nudge = $this->GetFirst($this->axis_font_size_v, $this->axis_font_size) / 2; if($c == -1 && $this->start_angle % 360 == 90) { $position['y'] -= $y_nudge; } elseif($c == 1 && $this->start_angle % 360 == 270) { $position['y'] += $y_nudge; } $position['text'] = $key; $position['w'] = $size[0]; $position['h'] = $size[1]; $positions[] = $position; } } return $positions; } /** * Text labels for the wrapped X-axis */ protected function XAxisText(&$points, $xoff, $yoff, $angle) { $inside = ('inside' == $this->GetFirst($this->axis_text_position_h, $this->axis_text_position)); $font_size = $this->GetFirst($this->axis_font_size_h, $this->axis_font_size); $positions = $this->XAxisTextPositions($points, $xoff, $yoff, $angle, $inside); $labels = ''; foreach($positions as $pos) { $text = $pos['text']; unset($pos['w'], $pos['h'], $pos['text'], $pos['angle'], $pos['sin'], $pos['cos']); $labels .= $this->Text($text, $font_size, $pos); } $group = array(); if(!empty($this->axis_font_h)) $group['font-family'] = $this->axis_font_h; if(!empty($this->axis_font_size_h)) $group['font-size'] = $font_size; if(!empty($this->axis_text_colour_h)) $group['fill'] = $this->axis_text_colour_h; if(empty($group)) return $labels; return $this->Element('g', $group, NULL, $labels); } /** * Returns the positions of the Y-axis text */ protected function YAxisTextPositions(&$points, $xoff, $yoff, $angle, $inside, $axis_no) { $positions = array(); $labels = ''; $font_size = $this->GetFirst($this->axis_font_size_v, $this->axis_font_size); $font_adjust = $this->GetFirst($this->axis_font_adjust_v, $this->axis_font_adjust); $text_space = $this->GetFirst($this->axis_text_space_v, $this->axis_text_space); $c = cos($this->arad); $s = sin($this->arad); $a = $this->arad + ($s * $c > 0 ? - M_PI_2 : M_PI_2); $x2 = ($xoff + $text_space) * sin($a); $y2 = ($xoff + $text_space) * cos($a); $x3 = 0; $y3 = $c > 0 ? $font_size : 0; $position = array('text-anchor' => $s < 0 ? 'start' : 'end'); foreach($points as $grid_point) { $key = $grid_point->text; $y = $grid_point->position; if(SVGGraphStrlen($key, $this->encoding) > 0) { $x1 = $y * $s; $y1 = $y * $c; $position['x'] = $this->xc + $x1 + $x2 + $x3; $position['y'] = $this->yc + $y1 + $y2 + $y3; if($angle != 0) { $rcx = $position['x']; $rcy = $position['y']; $position['transform'] = "rotate($angle,$rcx,$rcy)"; } $size = $this->TextSize((string)$key, $font_size, $font_adjust, $this->encoding, $angle, $font_size); $position['text'] = $key; $position['w'] = $size[0]; $position['h'] = $size[1]; $positions[] = $position; } } return $positions; } /** * Text labels for the Y-axis */ protected function YAxisText(&$points, $xoff, $yoff, $angle, $right, $axis_no) { $positions = $this->YAxisTextPositions($points, $xoff, $yoff, $angle, false, $axis_no); $labels = ''; $font_size = $this->GetFirst( $this->ArrayOption($this->axis_font_size_v, $axis_no), $this->axis_font_size); $anchor = $positions[0]['text-anchor']; foreach($positions as $pos) { $text = $pos['text']; unset($pos['w'], $pos['h'], $pos['text'], $pos['text-anchor']); $labels .= $this->Text($text, $font_size, $pos); } $group = array('text-anchor' => $anchor); if(!empty($this->axis_font_v)) $group['font-family'] = $this->ArrayOption($this->axis_font_v, $axis_no); if(!empty($this->axis_font_size_v)) $group['font-size'] = $font_size; if(!empty($this->axis_text_colour_v)) $group['fill'] = $this->ArrayOption($this->axis_text_colour_v, $axis_no); return $this->Element('g', $group, NULL, $labels); } /** * Returns what would be the vertical axis label */ protected function VLabel(&$attribs) { if(empty($this->label_v)) return ''; $c = cos($this->arad); $s = sin($this->arad); $a = $this->arad + ($s * $c > 0 ? - M_PI_2 : M_PI_2); $offset = max($this->division_size * (int)$this->show_divisions, $this->subdivision_size * (int)$this->show_subdivisions) + $this->pad_v_axis_label + $this->label_space; $offset += ($c < 0 ? ($this->CountLines($this->label_v) - 1) : 1) * $this->label_font_size; $x2 = $offset * sin($a); $y2 = $offset * cos($a); $p = $this->radius / 2; $x = $this->xc + $p * sin($this->arad) + $x2; $y = $this->yc + $p * cos($this->arad) + $y2; $a = $s < 0 ? 180 - $this->start_angle : -$this->start_angle; $pos = array( 'x' => $x, 'y' => $y, 'transform' => "rotate($a,$x,$y)", ); return $this->Text($this->label_v, $this->label_font_size, array_merge($attribs, $pos)); } /** * Returns the grid points for a Y-axis */ protected function GetGridPointsY($axis) { $points = $this->y_axes[$axis]->GetGridPoints(0); foreach($points as $k => $p) $points[$k]->position = -$p->position; return $points; } /** * Returns the subdivisions for a Y-axis */ protected function GetSubDivsY($axis) { $points = $this->y_axes[$axis]->GetGridSubdivisions( $this->minimum_subdivision, $this->ArrayOption($this->minimum_units_y, $axis), 0, $this->ArrayOption($this->subdivision_v, $axis)); foreach($points as $k => $p) $points[$k]->position = -$p->position; return $points; } }