finished graphs
[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 .= "\n".$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                         return;
300                 
301                 $count = $data['count'];
302                 if($count > $this->max_branches)
303                         $count = $this->max_branches;
304                 $this->image = imagecreatetruecolor($count * $this->size, $this->size);
305                 $transparentIndex = imagecolorallocate($this->image, 0xFF, 0xFF, 0xFF);
306                 imagefill($this->image, 0, 0, $transparentIndex);
307                 
308                 $this->apply_data($data);
309                 
310                 imagecolortransparent($this->image, $transparentIndex);
311
312                 header('Content-Type: image/png');
313                 imagepng($this->image);
314                 imagedestroy($this->image);
315         }
316         
317         private function display_header($header) {
318                 $header = explode("\n",$header);
319                 $count = $header[0];
320                 $header = array_slice($header, 1);
321                 if($count > $this->max_branches)
322                         $count = $this->max_branches;
323                 if(!$this->header_height) {
324                         $maxlen = 0;
325                         foreach($header as $head) {
326                                 $head = explode(":", $head, 2);
327                                 $name = $head[1];
328                                 if(strlen($name) > $maxlen)
329                                         $maxlen = strlen($name);
330                         }
331                         $this->header_height = $maxlen * 2 + 15;
332                 }
333                 $image = imagecreatetruecolor($count * $this->size + 60, $this->header_height);
334                 $transparentIndex = imagecolorallocate($image, 217, 216, 209);
335                 imagefill($image, 0, 0, $transparentIndex);
336                 $branches = 0;
337                 foreach($header as $head) {
338                         $head = explode(":", $head, 2);
339                         $color = $this->get_color($head[0], true);
340                         $name = $head[1];
341                         $branches++;
342                         $color = imagecolorallocatealpha($image, $color[0], $color[1], $color[2], 0);
343                         imagettftext($image, 8, 28, ($head[0]-1) * $this->size + 10, $this->header_height-2, $color, realpath(dirname(__FILE__)."/../")."/res/arial.ttf", $name);
344                 }
345                 if(!$branches) die();
346                 imagecolortransparent($image, $transparentIndex);
347                 header('Content-Type: image/png');
348                 imagepng($image);
349                 imagedestroy($image);
350         }
351         
352         private function parse_data($data) {
353                 if(GitConfig::GITGRAPH_BASE64)
354                         $data = base64_decode($data);
355                 if(!preg_match("/^([0-9]+)([abc]{1})([0-9]+)\(([^\)]*)\)([a-z0-9,\|]*)(\(([^\)]*)\)|)/i", $data, $matches)) {
356                         if(preg_match("/head:(.*)/i", $data, $matches)) {
357                                 $this->display_header(substr($data, strlen("head:")));
358                         }
359                         return null;
360                 }
361                 
362                 $data = array();
363                 $data['dot'] = array();
364                 $data['dot']['pos'] = $matches[1];
365                 $data['dot']['type'] = $matches[2];
366                 $data['count'] = $matches[3];
367                 
368                 if($matches[4] != '')
369                         $data['l'] = explode(',', $matches[4]);
370                 else
371                         $data['l'] = array();
372                 
373                 $data['m'] = array();
374                 if($matches[5] != '') {
375                         foreach(explode('|', $matches[5]) as $m) {
376                                 $merge = array();
377                                 
378                                 if(!preg_match("/^([0-9]+)([rln]{1})([rln]{1})([0123]{1})(.*)/i", $m, $sm))
379                                         return null;
380                                 $merge['hl'] = array();
381                                 $merge['pos'] = $sm[1];
382                                 $merge['dd'] = $sm[2];
383                                 $merge['md'] = $sm[3];
384                                 $merge['ml'] = $sm[4];
385                                 if($sm[5] != '') {
386                                         $merge['hl'] = explode(',', $sm[5]);
387                                 }
388                                 $data['m'][] = $merge;
389                         }
390                 }
391                 if($matches[6] != '' && $matches[7] != '') {
392                         foreach(explode(',', $matches[7]) as $cswap) {
393                                 $cswap = explode("=", $cswap);
394                                 $this->color_swap[$cswap[0]] = $cswap[1];
395                         }
396                 }
397                 return $data;
398         }
399         
400         function image_set_color($src, $color) {
401                 imagesavealpha($src, true);
402                 imagealphablending($src, false);
403                 // scan image pixels
404                 for ($x = 0; $x < $this->size; $x++) {
405                         for ($y = 0; $y < $this->size; $y++) {
406                                 $src_pix = imagecolorat($src,$x,$y);
407                                 $src_pix_array = imagecolorsforindex($src, $src_pix);
408                                 
409                                 imagesetpixel($src, $x, $y, imagecolorallocatealpha($src, $color[0], $color[1], $color[2], $src_pix_array['alpha']));
410                         }
411                 }
412         }
413
414         function overlay_image($name, $left, $color = false) {
415                 $image2 = imagecreatefrompng($name);
416
417                 if($color) {
418                         $this->image_set_color($image2, $color);
419                 }
420                 imagecopyresampled($this->image, $image2, $left, 0, 0, 0, $this->size, $this->size, $this->tile_size, $this->tile_size);
421         }
422
423         function get_color($id, $text = false) {
424                 if(array_key_exists($id, $this->color_swap))
425                         $id = $this->color_swap[$id];
426                 $color_array = $this->colors[($id - 1) % count($this->colors)];
427                 if($text && is_array($color_array[0]) && $color_array[1])
428                         return $color_array[1];
429                 return (is_array($color_array[0]) ? $color_array[0] : $color_array);
430         }
431         
432         private function apply_data($data) {
433                 foreach($data['l'] as $l)
434                         $this->overlay_image("res/line.png", ($l-1) * $this->size, $this->get_color($l));
435                 foreach($data['m'] as $m) {
436                         if($m['dd'] == 'r')
437                                 $this->overlay_image("res/dot_merge_right.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($m['pos']));
438                         else if($m['dd'] == 'l')
439                                 $this->overlay_image("res/dot_merge_left.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($m['pos']));
440                         if($m['md'] == 'r')
441                                 $this->overlay_image("res/".(($m['ml'] & 1) ? "branch" : "merge")."_right.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
442                         else if($m['md'] == 'l')
443                                 $this->overlay_image("res/".(($m['ml'] & 1) ? "branch" : "merge")."_left.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
444                         if($m['ml'] == 0)
445                                 $this->overlay_image("res/line.png", ($m['pos'] - 1) * $this->size, $this->get_color($m['pos']));
446                         foreach($m['hl'] as $hl) {
447                                 $this->overlay_image("res/line_h.png", ($hl - 1) * $this->size, $this->get_color($m['pos']));
448                         }
449                 }
450                 if($data['dot']['type'] == 'a')
451                         $this->overlay_image("res/dot.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
452                 else if($data['dot']['type'] == 'b')
453                         $this->overlay_image("res/dot_merge.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
454                 else if($data['dot']['type'] == 'c')
455                         $this->overlay_image("res/dot_init.png", ($data['dot']['pos'] - 1) * $this->size, $this->get_color($data['dot']['pos']));
456                 
457         }
458         
459 }
460
461 ?>