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