6fdc069c8b2e1d92fd6c97acdb94c70388213aee
[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         }
356         
357         private function parse_data($data) {
358                 if(GitConfig::GITGRAPH_BASE64)
359                         $data = base64_decode($data);
360                 if(!preg_match("/^([0-9]+)([abc]{1})([0-9]+)\(([^\)]*)\)([a-z0-9,\|]*)(\(([^\)]*)\)|)/i", $data, $matches)) {
361                         if(preg_match("/head:(.*)/i", $data, $matches)) {
362                                 $this->display_header(substr($data, strlen("head:")));
363                         }
364                         return null;
365                 }
366                 
367                 $data = array();
368                 $data['dot'] = array();
369                 $data['dot']['pos'] = $matches[1];
370                 $data['dot']['type'] = $matches[2];
371                 $data['count'] = $matches[3];
372                 
373                 if($matches[4] != '')
374                         $data['l'] = explode(',', $matches[4]);
375                 else
376                         $data['l'] = array();
377                 
378                 $data['m'] = array();
379                 if($matches[5] != '') {
380                         foreach(explode('|', $matches[5]) as $m) {
381                                 $merge = array();
382                                 
383                                 if(!preg_match("/^([0-9]+)([rln]{1})([rln]{1})([0123]{1})(.*)/i", $m, $sm))
384                                         return null;
385                                 $merge['hl'] = array();
386                                 $merge['pos'] = $sm[1];
387                                 $merge['dd'] = $sm[2];
388                                 $merge['md'] = $sm[3];
389                                 $merge['ml'] = $sm[4];
390                                 if($sm[5] != '') {
391                                         $merge['hl'] = explode(',', $sm[5]);
392                                 }
393                                 $data['m'][] = $merge;
394                         }
395                 }
396                 if($matches[6] != '' && $matches[7] != '') {
397                         foreach(explode(',', $matches[7]) as $cswap) {
398                                 $cswap = explode("=", $cswap);
399                                 $this->color_swap[$cswap[0]] = $cswap[1];
400                         }
401                 }
402                 return $data;
403         }
404         
405         function image_set_color($src, $color) {
406                 imagesavealpha($src, true);
407                 imagealphablending($src, false);
408                 // scan image pixels
409                 for ($x = 0; $x < $this->size; $x++) {
410                         for ($y = 0; $y < $this->size; $y++) {
411                                 $src_pix = imagecolorat($src,$x,$y);
412                                 $src_pix_array = imagecolorsforindex($src, $src_pix);
413                                 
414                                 imagesetpixel($src, $x, $y, imagecolorallocatealpha($src, $color[0], $color[1], $color[2], $src_pix_array['alpha']));
415                         }
416                 }
417         }
418
419         function overlay_image($name, $left, $color = false) {
420                 $image2 = imagecreatefrompng($name);
421
422                 if($color) {
423                         $this->image_set_color($image2, $color);
424                 }
425                 imagecopyresampled($this->image, $image2, $left, 0, 0, 0, $this->size, $this->size, $this->tile_size, $this->tile_size);
426         }
427
428         function get_color($id, $text = false) {
429                 if(array_key_exists($id, $this->color_swap))
430                         $id = $this->color_swap[$id];
431                 $color_array = $this->colors[($id - 1) % count($this->colors)];
432                 if($text && is_array($color_array[0]) && $color_array[1])
433                         return $color_array[1];
434                 return (is_array($color_array[0]) ? $color_array[0] : $color_array);
435         }
436         
437         private function apply_data($data) {
438                 foreach($data['l'] as $l)
439                         $this->overlay_image("res/line.png", ($l-1) * $this->size, $this->get_color($l));
440                 foreach($data['m'] as $m) {
441                         if($m['dd'] == 'r')
442                                 $this->overlay_image("res/dot_merge_right.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($m['pos']));
443                         else if($m['dd'] == 'l')
444                                 $this->overlay_image("res/dot_merge_left.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($m['pos']));
445                         if($m['md'] == 'r')
446                                 $this->overlay_image("res/".(($m['ml'] & 1) ? "branch" : "merge")."_right.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
447                         else if($m['md'] == 'l')
448                                 $this->overlay_image("res/".(($m['ml'] & 1) ? "branch" : "merge")."_left.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
449                         if($m['ml'] == 0)
450                                 $this->overlay_image("res/line.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
451                         foreach($m['hl'] as $hl) {
452                                 $this->overlay_image("res/line_h.png", ($hl - 1) * $this->size, $this->get_color($m['pos']));
453                         }
454                 }
455                 if($data['dot']['type'] == 'a')
456                         $this->overlay_image("res/dot.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
457                 else if($data['dot']['type'] == 'b')
458                         $this->overlay_image("res/dot_merge.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
459                 else if($data['dot']['type'] == 'c')
460                         $this->overlay_image("res/dot_init.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
461                 
462         }
463         
464 }
465
466 ?>