fixed small graph error
[phpgitweb.git] / htdocs / lib / graph.class.php
1 <?php
2 /* graph.class.php - phpgitweb
3  * Copyright (C) 2011-2012  Philipp Kreil (pk910)
4  * 
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  * 
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License 
16  * along with this program. If not, see <http://www.gnu.org/licenses/>. 
17  */
18
19 class graph_data_generator {
20         const DOT_TYPE_NORMAL = 'a';
21         const DOT_TYPE_MERGE = 'b';
22         const DOT_TYPE_INIT = 'c';
23
24         private $data = array();
25         private $graph = array();
26         private $max_branches, $brach_id = 1, $branch_id = 1;
27         
28         public function __construct() {
29                 $this->max_branches = GitConfig::GITGRAPH_MAX_BRANCHES;
30                 $this->data['branches'] = array();
31                 $this->data['ubranches'] = array();
32         }
33         
34         public function add_branch($first_id, $name) {
35                 $existing = false;
36                 foreach($this->data['branches'] as &$branch) {
37                         if($branch['next'] == $first_id) {
38                                 $existing = true;
39                                 $branch['name'][] = $name;
40                                 break;
41                         }
42                 }
43                 unset($branch);
44                 if($existing)
45                         return;
46                 $name_arr = array();
47                 if($name)
48                         $name_arr[] = $name;
49                 $this->data['branches'][count($this->data['branches'])] = array(
50                         "id" => $this->brach_id++,
51                         "uid" => $this->branch_uid++,
52                         "active" => true,
53                         "sticky" => true,
54                         "name" => $name_arr,
55                         "next" => $first_id,
56                         "pre_merge" => false
57                 );
58         }
59         
60         public function parse($commits) {
61                 if(!GitConfig::GITGRAPH_ENABLE)
62                         return;
63                 $brach_id = $this->brach_id;
64                 $branch_uid = $this->branch_id;
65                 $first_commit = (count($this->data['branches']) == 0 ? true : false);
66                 foreach($commits as $commit) {
67                         //get current branch
68                         $commit['merge'] = array();
69                         $commit['dot_type'] = self::DOT_TYPE_NORMAL;
70                         if($first_commit) {
71                                 $first_commit = false;
72                                 $this->data['branches'][0] = array();
73                                 $branch = &$this->data['branches'][0];
74                                 $branch['id'] = $brach_id++;
75                                 $branch['uid'] = $branch_uid++;
76                                 $branch['active'] = true;
77                         } else {
78                                 $first = true;
79                                 foreach($this->data['branches'] as $id => &$cbranch) {
80                                         if($cbranch['next'] == $commit['id']) {
81                                                 if($first && !$cbranch['pre_merge']) {
82                                                         $branch = &$this->data['branches'][$id];
83                                                         $first = false;
84                                                 }
85                                         }
86                                 }
87                                 foreach($this->data['branches'] as $id => &$cbranch) {
88                                         if($cbranch['next'] == $commit['id']) {
89                                                 if($first) {
90                                                         $branch = &$this->data['branches'][$id];
91                                                         $first = false;
92                                                 } else if($cbranch['id'] == $branch['id']) {
93                                                 } else {
94                                                         $commit['merge'][] = array("point" => $cbranch['id'], "start" => true, "end" => false);
95                                                         $cbranch['active'] = false;
96                                                         if($cbranch['pre_merge']) {
97                                                                 $cbranch['pre_merge_start'] = true;
98                                                                 $cbranch['pre_merge_id'] = $branch['id'];
99                                                                 $this->data['ubranches'][$cbranch['uid']] = $this->data['branches'][$cbranch['id']-1];
100                                                         }
101                                                 }
102                                         }
103                                 }
104                                 unset($cbranch);
105                                 if($first) {
106                                         $this->data['branches'][count($this->data['branches'])] = array();
107                                         $branch = &$this->data['branches'][count($this->data['branches'])-1];
108                                         $branch['id'] = $brach_id++;
109                                         $branch['uid'] = $branch_uid++;
110                                         $branch['active'] = true;
111                                         $branch['pre_merge'] = false;
112                                         
113                                 }
114                         }
115                         
116                         if(array_key_exists('parent', $commit) && count($commit['parent']) > 1) {
117                                 //merge(s)
118                                 for($j = 1; $j < count($commit['parent']); $j++) {
119                                         $add = true;
120                                         foreach($this->data['branches'] as $cbranch) {
121                                                 if(array_key_exists('next', $cbranch) && $cbranch['next'] == $commit['parent'][$j]) {
122                                                         $add = false;
123                                                         break;
124                                                 }
125                                         }
126                                         if($add) {
127                                                 $cadd = true;
128                                                 foreach($this->data['branches'] as $bid => &$cbranch) {
129                                                         if(!$cbranch['active']) {
130                                                                 $cadd = false;
131                                                                 break;
132                                                         }
133                                                 }
134                                                 if($cadd) {
135                                                         $this->data['branches'][count($this->data['branches'])] = array();
136                                                         $cbranch = &$this->data['branches'][count($this->data['branches'])-1];
137                                                         $cbranch['id'] = $brach_id++;
138                                                 }
139                                                 $cbranch['uid'] = $branch_uid++;
140                                                 $cbranch['active'] = true;
141                                                 $cbranch['pre_merge'] = true;
142                                                 $cbranch['next'] = $commit['parent'][$j];
143                                         }
144                                         $commit['merge'][] = array("point" => $cbranch['id'], "start" => false, "end" => $add);
145                                         $commit['dot_type'] = self::DOT_TYPE_MERGE;
146                                         $this->data['ubranches'][$cbranch['uid']] = $this->data['branches'][$cbranch['id']-1];
147                                         unset($cbranch);
148                                 }
149                         } else if(!array_key_exists('parent', $commit) || count($commit['parent']) == 0) {
150                                 $branch['active'] = false;
151                                 $commit['dot_type'] = self::DOT_TYPE_INIT;
152                         }
153                         $branch['next'] = ((array_key_exists('parent', $commit) && count($commit['parent'])) ? $commit['parent'][0] : null);
154                         $branch['pre_merge'] = false;
155                         $this->data['ubranches'][$branch['uid']] = $this->data['branches'][$branch['id']-1];
156                         
157                         $commit['dot'] = $branch['id'];
158                         
159                         foreach($this->data['branches'] as $id => $cbranch) {
160                                 $commit['branches'][$id] = $cbranch;
161                         }
162                         
163                         $this->graph[$commit['id']] = $this->graph_data($commit);
164                         //echo$commit['id']." ".$this->get_graph($commit['id'])."\n";
165                 }
166         }
167         
168         private function graph_data($commit) {
169                 $data = array();
170                 $data['d'] = array();
171                 $data['d']['p'] = $commit['dot']; //dot position
172                 $data['d']['type'] = 'a';
173                 $data['l'] = array(); //lines
174                 $data['br'] = array(); //branches for color check
175                 foreach($commit['branches'] as $branch) {
176                         if($branch['pre_merge'] || $commit['merge']) {
177                                 $data['br'][] = $branch['uid'];
178                         }
179                         if($branch['active']) {
180                                 if($commit['dot'] == $branch['id']) continue;
181                                 $show = true;
182                                 if($commit['merge']) {
183                                         foreach($commit['merge'] as $merge) {
184                                                 if($merge['point'] == $branch['id']) {
185                                                         $show = false;
186                                                         break;
187                                                 }
188                                         }
189                                 }
190                                 if(!$show) continue;
191                                 if($branch['id'] > $this->max_branches) continue;
192                                 $data['l'][] = $branch['id'];
193                         }
194                 }
195                 $data['m'] = array(); //merges
196                 if($commit['merge']) {
197                         foreach($commit['merge'] as $merge) {
198                                 $mergepoint = array();
199                                 $mergepoint['hl'] = array();
200                                 $mergepoint['p'] = $merge['point'];
201                                 
202                                 if($commit['dot'] <= $this->max_branches)
203                                         $mergepoint['dd'] = ($commit['dot'] < $merge['point'] ? 'r' : 'l');
204                                 else
205                                         $mergepoint['dd'] = 'n';
206                                 
207                                 $mergepoint['ml'] = ($merge['start'] ? 1 : 0) + ($merge['end'] ? 2 : 0);
208                                 if($merge['point'] <= $this->max_branches)
209                                         $mergepoint['md']=($commit['dot'] < $merge['point'] ? 'l' : 'r');
210                                 else
211                                         $mergepoint['md'] = 'n';
212                                 $min = ($commit['dot'] < $merge['point'] ? $commit['dot'] : $merge['point']) + 1;
213                                 $max = ($commit['dot'] < $merge['point'] ? $merge['point'] : $commit['dot']);
214                                 for($i = $min; $i < $max; $i++) {
215                                         if($i > $this->max_branches) continue;
216                                         $mergepoint['hl'][] = $i;
217                                 }
218                                 $data['m'][] = $mergepoint;
219                         }
220                 }
221                 $data['d']['type'] = $commit['dot_type'];
222                 return $data;
223         }
224         
225         public function get_graph($id) {
226                 if(!GitConfig::GITGRAPH_ENABLE)
227                         return;
228                 $graph = $this->graph[$id];
229                 $data = $graph['d']['p'].$graph['d']['type'].count($this->data['branches']).'('.implode(',', $graph['l']).')';
230                 $first_merge = true;
231                 foreach($graph['m'] as $merge) {
232                         if(!$first_merge)
233                                 $data .= '|';
234                         $first_merge = false;
235                         $data.=$merge['p'].$merge['dd'].$merge['md'].$merge['ml'];
236                         foreach($merge['hl'] as $hline)
237                                 $data.=','.$hline;
238                 }
239                 $graph['cs'] = array();
240                 foreach($graph['br'] as $buid) {
241                         if(!$buid) continue;
242                         $branch = $this->data['ubranches'][$buid];
243                         if($branch['pre_merge'] && array_key_exists('pre_merge_start', $branch) && $branch['pre_merge_start'])
244                                 $graph['cs'][] = $branch['id']."=".$branch['pre_merge_id'];
245                 }
246                 if(count($graph['cs'])) {
247                         $data .= '('.implode(',', $graph['cs']).')';
248                 }
249                 if(GitConfig::GITGRAPH_BASE64)
250                         $data = base64_encode($data);
251                 return $data;
252         }
253         
254         public function get_header_graph() {
255                 $data = "head:";
256                 $branchcount = 0;
257                 $dataadd = "";
258                 foreach($this->data['branches'] as $branch) {
259                         if(array_key_exists('sticky', $branch) && $branch['sticky']) {
260                                 $name = explode('/', $branch['name'][0], 3);
261                                 $dataadd .= "//".$branch['id'].":".$name[2];
262                                 $branchcount++;
263                         }
264                 }
265                 $data.=$branchcount.$dataadd;
266                 if(GitConfig::GITGRAPH_BASE64)
267                         $data = base64_encode($data);
268                 return $data;
269         }
270 }
271
272 class graph_image_generator {
273         private $max_branches;
274         private $image;
275         private $size;
276         private $tile_size;
277         private $header_height = false;
278         private $colors, $color_swap = array();
279         
280         public function __construct() {
281                 $this->max_branches = GitConfig::GITGRAPH_MAX_BRANCHES;
282                 $this->size = GitConfig::GITGRAPH_END_SIZE;
283                 $this->tile_size = GitConfig::GITGRAPH_TILE_SIZE;
284                 
285                 $this->colors = array(
286                         NULL,
287                         array(255, 0, 0),
288                         array(array(0, 255, 0), array(0, 192, 0)),
289                         array(0, 0, 255),
290                         array(128, 128, 128),
291                         array(128, 128, 0),
292                         array(0, 128, 128),
293                         array(128, 0, 128)
294                 );
295         }
296         
297         public function generate($data) {
298                 if(!GitConfig::GITGRAPH_ENABLE)
299                         return;
300                 $data = $this->parse_data($data);
301                 if(!$data) {
302                         header('Content-Type: text/plain');
303                         die(base64_decode("ICAgIC0tLS0tLS0tOi0tLS0tLS0tDQogICAgICAgICAgLC0iLC5fX19fX19fIC8NCiAgICAgICAgIC8gKSB8ICAgLC0tLS0nXA0KICAgICAgICAgXC9fX3wuLSINCiAgICAgICAgLl8vL19cXF8NCk5vdCB3aGF0IHlvdSBleHBlY3RlZCwgZWVlaD8="));
304                 }
305                 
306                 $count = $data['count'];
307                 if($count > $this->max_branches)
308                         $count = $this->max_branches;
309                 $this->image = imagecreatetruecolor($count * $this->size, $this->size);
310                 $transparentIndex = imagecolorallocate($this->image, 0xFF, 0xFF, 0xFF);
311                 imagefill($this->image, 0, 0, $transparentIndex);
312                 
313                 $this->apply_data($data);
314                 
315                 imagecolortransparent($this->image, $transparentIndex);
316
317                 header('Content-Type: image/png');
318                 imagepng($this->image);
319                 imagedestroy($this->image);
320         }
321         
322         private function display_header($header) {
323                 $header = explode("//",$header);
324                 $count = $header[0];
325                 $header = array_slice($header, 1);
326                 if($count > $this->max_branches)
327                         $count = $this->max_branches;
328                 if(!$this->header_height) {
329                         $maxlen = 0;
330                         foreach($header as $head) {
331                                 $head = explode(":", $head, 2);
332                                 $name = $head[1];
333                                 if(strlen($name) > $maxlen)
334                                         $maxlen = strlen($name);
335                         }
336                         $this->header_height = $maxlen * 2 + 15;
337                 }
338                 $image = imagecreatetruecolor($count * $this->size + 60, $this->header_height);
339                 $transparentIndex = imagecolorallocate($image, 217, 216, 209);
340                 imagefill($image, 0, 0, $transparentIndex);
341                 $branches = 0;
342                 foreach($header as $head) {
343                         $head = explode(":", $head, 2);
344                         $color = $this->get_color($head[0], true);
345                         $name = $head[1];
346                         $branches++;
347                         $color = imagecolorallocatealpha($image, $color[0], $color[1], $color[2], 0);
348                         imagettftext($image, 8, 28, ($head[0]-1) * $this->size + 10, $this->header_height-2, $color, realpath(dirname(__FILE__)."/../")."/res/arial.ttf", $name);
349                 }
350                 if(!$branches) die();
351                 imagecolortransparent($image, $transparentIndex);
352                 header('Content-Type: image/png');
353                 imagepng($image);
354                 imagedestroy($image);
355                 die();
356         }
357         
358         private function parse_data($data) {
359                 if(GitConfig::GITGRAPH_BASE64)
360                         $data = base64_decode($data);
361                 if(!preg_match("/^([0-9]+)([abc]{1})([0-9]+)\(([^\)]*)\)([a-z0-9,\|]*)(\(([^\)]*)\)|)/i", $data, $matches)) {
362                         if(preg_match("/head:(.*)/i", $data, $matches)) {
363                                 $this->display_header(substr($data, strlen("head:")));
364                         }
365                         return null;
366                 }
367                 
368                 $data = array();
369                 $data['dot'] = array();
370                 $data['dot']['pos'] = $matches[1];
371                 $data['dot']['type'] = $matches[2];
372                 $data['count'] = $matches[3];
373                 
374                 if($matches[4] != '')
375                         $data['l'] = explode(',', $matches[4]);
376                 else
377                         $data['l'] = array();
378                 
379                 $data['m'] = array();
380                 if($matches[5] != '') {
381                         foreach(explode('|', $matches[5]) as $m) {
382                                 $merge = array();
383                                 
384                                 if(!preg_match("/^([0-9]+)([rln]{1})([rln]{1})([0123]{1})(.*)/i", $m, $sm))
385                                         return null;
386                                 $merge['hl'] = array();
387                                 $merge['pos'] = $sm[1];
388                                 $merge['dd'] = $sm[2];
389                                 $merge['md'] = $sm[3];
390                                 $merge['ml'] = $sm[4];
391                                 if($sm[5] != '') {
392                                         $merge['hl'] = explode(',', $sm[5]);
393                                 }
394                                 $data['m'][] = $merge;
395                         }
396                 }
397                 if($matches[6] != '' && $matches[7] != '') {
398                         foreach(explode(',', $matches[7]) as $cswap) {
399                                 $cswap = explode("=", $cswap);
400                                 $this->color_swap[$cswap[0]] = $cswap[1];
401                         }
402                 }
403                 return $data;
404         }
405         
406         function image_set_color($src, $color) {
407                 imagesavealpha($src, true);
408                 imagealphablending($src, false);
409                 // scan image pixels
410                 for ($x = 0; $x < $this->size; $x++) {
411                         for ($y = 0; $y < $this->size; $y++) {
412                                 $src_pix = imagecolorat($src,$x,$y);
413                                 $src_pix_array = imagecolorsforindex($src, $src_pix);
414                                 
415                                 imagesetpixel($src, $x, $y, imagecolorallocatealpha($src, $color[0], $color[1], $color[2], $src_pix_array['alpha']));
416                         }
417                 }
418         }
419
420         function overlay_image($name, $left, $color = false) {
421                 $image2 = imagecreatefrompng($name);
422
423                 if($color) {
424                         $this->image_set_color($image2, $color);
425                 }
426                 imagecopyresampled($this->image, $image2, $left, 0, 0, 0, $this->size, $this->size, $this->tile_size, $this->tile_size);
427         }
428
429         function get_color($id, $text = false) {
430                 if(array_key_exists($id, $this->color_swap))
431                         $id = $this->color_swap[$id];
432                 $color_array = $this->colors[($id - 1) % count($this->colors)];
433                 if($text && is_array($color_array[0]) && $color_array[1])
434                         return $color_array[1];
435                 return (is_array($color_array[0]) ? $color_array[0] : $color_array);
436         }
437         
438         private function apply_data($data) {
439                 foreach($data['l'] as $l)
440                         $this->overlay_image("res/line.png", ($l-1) * $this->size, $this->get_color($l));
441                 foreach($data['m'] as $m) {
442                         if($m['dd'] == 'r')
443                                 $this->overlay_image("res/dot_merge_right.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($m['pos']));
444                         else if($m['dd'] == 'l')
445                                 $this->overlay_image("res/dot_merge_left.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($m['pos']));
446                         if($m['md'] == 'r')
447                                 $this->overlay_image("res/".(($m['ml'] & 1) ? "branch" : "merge")."_right.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
448                         else if($m['md'] == 'l')
449                                 $this->overlay_image("res/".(($m['ml'] & 1) ? "branch" : "merge")."_left.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
450                         if($m['ml'] == 0)
451                                 $this->overlay_image("res/line.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
452                         foreach($m['hl'] as $hl) {
453                                 $this->overlay_image("res/line_h.png", ($hl - 1) * $this->size, $this->get_color($m['pos']));
454                         }
455                 }
456                 if($data['dot']['type'] == 'a')
457                         $this->overlay_image("res/dot.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
458                 else if($data['dot']['type'] == 'b')
459                         $this->overlay_image("res/dot_merge.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
460                 else if($data['dot']['type'] == 'c')
461                         $this->overlay_image("res/dot_init.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
462                 
463         }
464         
465 }
466
467 ?>