]> dev.renevier.net Git - syj.git/blob - application/controllers/ProfileController.php
routes profile
[syj.git] / application / controllers / ProfileController.php
1 <?php
2 /*  This file is part of Syj, Copyright (c) 2010-2011 Arnaud Renevier,
3     and is published under the AGPL license. */
4
5 class Canvas {
6     protected $picture;
7
8     protected $width;
9     protected $height;
10     protected $margint;
11     protected $marginr;
12     protected $marginb;
13     protected $marginl;
14
15     protected $minx;
16     protected $miny;
17     protected $maxx;
18     protected $maxy;
19     protected $xdelta;
20     protected $ydelta;
21
22     protected $fontfile;
23     protected $fontsize;
24
25     function __construct($width, $height, $margint, $marginr, $marginb, $marginl) {
26         $this->width = $width;
27         $this->height = $height;
28         $this->margint = $margint;
29         $this->marginr = $marginr;
30         $this->marginb = $marginb;
31         $this->marginl = $marginl;
32
33         $this->minx = 0;
34         $this->miny = 0;
35         $this->maxx = $this->width;
36         $this->maxy = $this->height;
37
38         $this->picture = imagecreatetruecolor ($this->width, $this->height);
39     }
40
41     function __destruct() {
42         if ($this->picture) {
43             imagedestroy ($this->picture);
44         }
45     }
46
47     function setViewBox($minx, $miny, $maxx, $maxy) {
48         $this->xdelta = pow(10, floor(log10($maxx)));
49         $this->maxx = ceil ($maxx / $this->xdelta) * $this->xdelta;
50         $this->minx = floor ($minx / $this->xdelta) * $this->xdelta;
51
52         $this->ydelta = pow(10, floor(log10($maxy)));
53         $this->maxy = ceil ($maxy / $this->ydelta) * $this->ydelta;
54         $this->miny = floor ($miny / $this->ydelta) * $this->ydelta;
55
56         return $this;
57     }
58
59     function getViewBox() {
60         return array($this->minx, $this->miny, $this->maxx, $this->maxy);
61     }
62
63     function setThickness($thickness) {
64         imagesetthickness ($this->picture, $thickness);
65
66         return $this;
67     }
68
69     function setFont($fontfile, $fontsize) {
70         $this->fontfile = $fontfile;
71         $this->fontsize = $fontsize;
72
73         return $this;
74     }
75
76     function text($x, $y, $text, $color, $pos = "lt", $limits = array(null, null, null, null)) {
77         $bbox = imagettfbbox($this->fontsize, 0, $this->fontfile, $text);
78
79         switch (substr($pos, 0, 1)) {
80             case "r":
81                 $x -= $bbox[2] - $bbox[0];
82             break;
83             case "c":
84                 $x -= ($bbox[2] - $bbox[0]) / 2;
85             break;
86             case "l":
87             default:
88             break;
89         }
90
91         switch (substr($pos, 1, 1)) {
92             case "b":
93                 $y -= $bbox[7] - $bbox[1];
94             break;
95             case "c":
96                 $y -= ($bbox[7] - $bbox[1]) / 2;
97             break;
98             case "t":
99             default:
100             break;
101         }
102
103         list($limtop, $limright, $limbottom, $limleft) = $limits;
104
105         if (!is_null($limtop)) {
106             $y = max($limtop, $y);
107         }
108         if (!is_null($limright)) {
109             $x = min($limright, $x);
110         }
111         if (!is_null($limbottom)) {
112             $y = min($limbottom, $y);
113         }
114         if (!is_null($limleft)) {
115             $x = max($limleft, $x);
116         }
117
118         imagettftext($this->picture, $this->fontsize, 0, $x, $y, $this->color($color), $this->fontfile, $text);
119     }
120
121     function rect($x1, $y1, $x2, $y2, $color, $translate = false) {
122         imagefilledrectangle ($this->picture, $x1, $y1, $x2, $y2, $this->color($color));
123
124         return $this;
125     }
126
127     function line($x1, $y1, $x2, $y2, $color) {
128         imageline ($this->picture, $x1, $y1, $x2, $y2, $this->color($color));
129
130         return $this;
131     }
132
133     function draw($filename = null) {
134         imagepng($this->picture, $filename);
135
136         return $this;
137     }
138
139     private $colorcache = array();
140     function color($str) {
141         if (!isset ($this->colorcache[$str])) {
142             $r = "0x" . substr($str, 0, 2);
143             $g = "0x" . substr($str, 2, 2);
144             $b = "0x" . substr($str, 4, 2);
145             $this->colorcache[$str] = imagecolorallocate ($this->picture, $r, $g, $b);
146         }
147         return $this->colorcache[$str];
148     }
149
150     function canvasXPos($x) {
151         return round(($x - $this->minx) * ($this->width - $this->marginr - $this->marginl) / ($this->maxx - $this->minx)) + $this->marginl;
152     }
153     function canvasYPos($y) {
154         return $this->height - round(($y - $this->miny) * ($this->height - $this->marginb - $this->margint) / ($this->maxy - $this->miny)) - $this->margint;
155     }
156 }
157
158 class ProfileController extends Zend_Controller_Action {
159     public function init() {
160         if (!extension_loaded('gd')) {
161             throw new Syj_Exception_NotImplemented("gd is not installed");
162         }
163         $this->_helper->SyjNoRender->disableRender();
164     }
165
166     public function indexAction() {
167         $url = $this->getRequest()->getUserParam('url');
168         if (!isset($url)) {
169             throw new Syj_Exception_NotFound('Not Found', 404);
170         }
171
172         $pathMapper = new Syj_Model_PathMapper();
173         $path = new Syj_Model_Path();
174         if (!$pathMapper->findByUrl($url, $path)) {
175             if (is_numeric($url) and $pathMapper->hasexisted($url)) {
176                 throw new Syj_Exception_NotFound('Gone', 410);
177             } else {
178                 throw new Syj_Exception_NotFound('Not Found', 404);
179             }
180         }
181
182         $size = $this->getRequest()->getQuery('size', 'big');
183         if ($size == 'small') {
184             $width = 300;
185             $height = 225;
186         } else {
187             $width = 800;
188             $height = 600;
189             $size = 'big';
190         }
191
192         $file = $path->getProfileCache($size);
193         if (file_exists($file)) {
194             if (filesize($file) == 0) {
195                 throw new Syj_Exception_NotImplemented("could not compute altitude profile");
196             }
197             $this->sendFile($file);
198             return;
199         }
200
201         try {
202             $service = $this->_helper->SyjAltiService->service();
203             /* we accept 2% of invalid values in the profile */
204             $profile = $path->getAltiProfile($service, 2 / 100);
205         } catch(Syj_Exception_NotImplemented $e) {
206             @touch($file);
207             throw $e;
208         }
209
210         $canvas = $this->drawProfile($profile, $width, $height);
211         $canvas->draw($file);
212         $this->sendFile($file);
213     }
214
215     protected function drawProfile($profile, $width, $height) {
216         $margint = 50;
217         $marginr = 50;
218         $marginb = 50;
219         $marginl = 50;
220
221         $last = end($profile);
222         $maxdist = $last[0];
223
224         $minalti = array(array(0, INF));
225         $maxalti = array(array(0, -INF));
226         foreach ($profile as $coords) {
227             $alti = $coords[1];
228
229             if ($alti == $minalti[0][1]) {
230                 $minalti []= array($coords[0], $alti);
231             } else if ($alti < $minalti[0][1]) {
232                 $minalti = array(array($coords[0], $alti));
233             }
234
235             if ($alti == $maxalti[0][1]) {
236                 $maxalti []= array($coords[0], $alti);
237             } else if ($alti > $maxalti[0][1]) {
238                 $maxalti = array(array($coords[0], $alti));
239             }
240         }
241
242         $canvas = new Canvas($width, $height, $margint, $marginr, $marginb, $marginl);
243         $canvas->setViewBox(0, $minalti[0][1], $maxdist, $maxalti[0][1]);
244
245         $canvas->rect(0, 0, $width, $height, "FFFFFF");
246
247         $slopecolor = function ($slope) {
248             if ($slope > 18) {
249                 return "FF0000"; // red
250             } else if ($slope > 12) {
251                 return "FF4000";
252             } else if ($slope > 7) {
253                 return "FF8000";
254             } else if ($slope > 3) {
255                 return "FFC000";
256             } else if ($slope > 0) {
257                 return "FFFF00";  // yellow
258             } else if ($slope > -3) {
259                 return "00FFFF";
260             } else if ($slope > -7) {
261                 return "00C0FF";
262             } else if ($slope > -12) {
263                 return "0080FF";
264             } else if ($slope > -18) {
265                 return "0040FF";
266             } else {
267                 return "0000FF"; // blue;
268             }
269         };
270
271         $canvas->setThickness (2);
272
273         $prev = null;
274         foreach ($profile as $coord) {
275             if (!is_null($prev)) {
276                 $dist = $coord[0];
277                 $alti = $coord[1];
278                 $prevdist = $prev[0];
279                 $prevalti = $prev[1];
280
281                 $slope = 100 * ($alti - $prevalti) / ($dist - $prevdist) ;
282                 $canvas->line($canvas->canvasXPos($prevdist), $canvas->canvasYPos($prevalti),
283                               $canvas->canvasXPos($dist), $canvas->canvasYPos($alti),
284                               $slopecolor($slope), true);
285             }
286             $prev = $coord;
287         }
288
289         $canvas->setThickness (1);
290         $canvas->setFont(APPLICATION_PATH . '/resources/' . 'DejaVuSans.ttf', 10);
291
292         list ($minx, $miny, $maxx, $maxy) = $canvas->getViewBox();
293
294         $deltax = pow(10, floor(log10($maxx - $minx)));
295         foreach (range($minx, $maxx, $deltax) as $x) {
296             $canvas->line($canvas->canvasXPos($x), $height - $marginb + 10, $canvas->canvasXPos($x), $height - $marginb, "000000");
297             if ($deltax < 1000) {
298                 $text = $x . "m";
299             } else {
300                 $text = round($x / 1000) . "km";
301             }
302             $canvas->text($canvas->canvasXPos($x), $height - $marginb + 10 + 3, $text, "000000", "cb");
303         }
304         $canvas->line($marginl, $height - $marginb, $width - $marginr, $height - $marginb, "000000");
305
306         $deltay = pow(10, floor(log10($maxy - $miny)));
307         foreach (range($miny, $maxy, $deltay) as $y) {
308             $canvas->line($marginl - 10, $canvas->canvasYPos($y), $marginl, $canvas->canvasYPos($y), "000000");
309             $text = $y . "m";
310             $canvas->text($marginl - 10 - 3, $canvas->canvasYPos($y), $text, "000000", "rc");
311         }
312         $canvas->line($marginl, $height - $marginb, $marginr, $margint, "000000");
313
314         list($minx, $miny, $maxx, $maxy) = $canvas->getViewBox();
315         $limits = array($canvas->canvasYPos($maxy) + 3, $canvas->canvasXPos($maxx) - 3, $canvas->canvasYPos($miny) - 3, $canvas->canvasXPos($minx) + 3);
316
317         $prev = null;
318         foreach ($minalti as $coords) {
319             list ($dist, $alti) = $coords;
320             if (!is_null($prev)) {
321                 $xdist = $canvas->canvasXPos($dist) - $canvas->canvasXPos($prev[0]);
322                 /* second label would be less than 10px from previous one. Do
323                 not display it */
324                 if ($xdist < 10) {
325                     continue;
326                 }
327             }
328             $text = round($alti) . "m";
329             $xpos = $canvas->canvasXPos($dist);
330             $ypos = $canvas->canvasYPos($alti);
331             $canvas->text($xpos, $ypos + 5, $text, "000000", "cb", $limits);
332             $canvas->rect($xpos - 1, $ypos - 1, $xpos + 1, $ypos + 1, "000000");
333             $prev = $coords;
334         }
335
336         $prev = null;
337         foreach ($maxalti as $coords) {
338             list ($dist, $alti) = $coords;
339             if (!is_null($prev)) {
340                 $xdist = $canvas->canvasXPos($dist) - $canvas->canvasXPos($prev[0]);
341                 /* second label would be less than 10px from previous one. Do
342                 not display it */
343                 if ($xdist < 10) {
344                     continue;
345                 }
346             }
347             $text = round($alti) . "m";
348             $xpos = $canvas->canvasXPos($dist);
349             $ypos = $canvas->canvasYPos($alti);
350             $canvas->text($xpos, $ypos - 5, $text, "000000", "ct", $limits);
351             $canvas->rect($xpos - 1, $ypos - 1, $xpos + 1, $ypos + 1, "000000");
352             $prev = $coords;
353         }
354
355         return $canvas;
356     }
357
358     protected function sendFile($file) {
359         $lastmodified = filemtime($file);
360
361         $request = $this->getRequest();
362         $response = $this->getResponse();
363
364         $response->setHeader('Content-Type', 'image/png', true)
365                  ->setHeader('Content-Length', filesize($file), true);
366
367         if ($request->getServer("HTTP_IF_MODIFIED_SINCE")) {
368             if ($lastmodified <= strtotime($request->getServer("HTTP_IF_MODIFIED_SINCE"))) {
369                 $response->setHttpResponseCode(304);
370                 return;
371             }
372         }
373
374                  // no-cache is needed otherwise IE does not try to get new version.
375         $response->setHeader ('Cache-control', 'no-cache, must-revalidate', true)
376                  ->setHeader("Last-Modified", gmdate("D, d M Y H:i:s", filemtime($file)) . " GMT", true);
377
378         readfile($file);
379     }
380 }