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