From: Arno Renevier Date: Wed, 28 Dec 2011 17:18:24 +0000 (+0100) Subject: routes profile X-Git-Tag: v0.4~4 X-Git-Url: https://dev.renevier.net/?p=syj.git;a=commitdiff_plain;h=00c2579ade64a20ba2d82e98d3eea5f864864cdb routes profile --- diff --git a/.gitmodules b/.gitmodules index dea91ea..e2e26fa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "public/store"] path = public/store url = git://github.com/cloudhead/store.js.git +[submodule "library/altiphp"] + path = library/altiphp + url = https://github.com/arenevier/altiphp.git diff --git a/COPYING.TXT b/COPYING.TXT index e83ada2..56f5a14 100644 --- a/COPYING.TXT +++ b/COPYING.TXT @@ -14,6 +14,8 @@ layer-switcher-maximize.png from OpenLayers. As such it is licensed under a modified BSD license. +- DejaVuSans.ttf is part of dejavu and is public domain. + =============================================================================== diff --git a/application/Bootstrap.php b/application/Bootstrap.php index f3fb399..0d0d4cf 100644 --- a/application/Bootstrap.php +++ b/application/Bootstrap.php @@ -3,7 +3,7 @@ and is published under the AGPL license. */ class Bootstrap extends Zend_Application_Bootstrap_Bootstrap { - protected static $libns = array('gisconverter', 'phptojs', 'pwdgen'); + protected static $libns = array('gisconverter', 'phptojs', 'pwdgen', 'alti'); public function _bootstrap($resource = null) { $autoloader = Zend_Loader_Autoloader::getInstance(); diff --git a/application/configs/application.ini b/application/configs/application.ini index 58eb837..ef2aba5 100644 --- a/application/configs/application.ini +++ b/application/configs/application.ini @@ -25,6 +25,10 @@ resources.frontController.plugins.loggeduser = "Syj_Controllers_Plugins_SyjLogge resources.frontController.plugins.contenttype = "Syj_Controllers_Plugins_SyjContentType" resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts/" resources.db.adapter = "Pdo_Pgsql" +; altiphp +resources.frontController.params.profileCache = APPLICATION_PATH "/../generated/profile/" +resources.frontController.altiphp.source = "srtmtiles" +resources.frontController.altiphp.cache = APPLICATION_PATH "/../generated/tiles" ; i18n resources.translate.adapter = gettext resources.translate.data = APPLICATION_PATH "/languages" @@ -109,6 +113,12 @@ resources.router.routes.faq.defaults.action = "index" resources.router.routes.nominatim.route = "nominatim/" resources.router.routes.nominatim.defaults.controller = "nominatim" resources.router.routes.nominatim.defaults.action = "index" +; profile/ +resources.router.routes.profile.type = "Zend_Controller_Router_Route_Regex" +resources.router.routes.profile.route = "profile/(.*)\.png" +resources.router.routes.profile.map.1 = "url" +resources.router.routes.profile.defaults.controller = "profile" +resources.router.routes.profile.defaults.action = "index" ; database resources.db.params.username = "arno" diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php index 043b199..0a6c468 100644 --- a/application/controllers/ErrorController.php +++ b/application/controllers/ErrorController.php @@ -28,6 +28,8 @@ class ErrorController extends Zend_Controller_Action $error_code = 400; // Bad Request } else if ($error->exception instanceof Syj_Exception_Forbidden) { $error_code = 403; // Forbidden + } else if ($error->exception instanceof Syj_Exception_NotImplemented) { + $error_code = 501; // Not Implemented } else if ($error->exception instanceof Syj_Exception_NotFound) { $error_code = $error->exception->getCode(); } diff --git a/application/controllers/IdxController.php b/application/controllers/IdxController.php index 9bfc6bf..997be3d 100644 --- a/application/controllers/IdxController.php +++ b/application/controllers/IdxController.php @@ -59,6 +59,7 @@ class IdxController extends Zend_Controller_Action $this->view->headScript()->prependScript((string) $jsgeom); $this->view->loginform->login_geom_id->setValue((string)$path->id); $this->view->geomform->geom_title->setValue($path->title); + $this->view->profileActive = $this->_hasAltiProfile($path); } else { $this->_setInitialPos(); $title = "Show your journey"; @@ -215,4 +216,30 @@ class IdxController extends Zend_Controller_Action return round($size) . $sizes[$c]; } + private function _hasAltiProfile($path) { + if (!extension_loaded('gd')) { + return false; + } + $cachefile = $path->getProfileCache('small'); + if (file_exists($cachefile)) { + return filesize($cachefile) != 0; + } + + try { + $service = $this->_helper->SyjAltiService->service(); + } catch(Exception $e) { + return false; + } + + try { + $path->getAltiProfile($service, 2 / 100); + return true; + } catch(Syj_Exception_NotImplemented $e) { + @touch($cachefile); + return false; + } catch(Exception $e) { + return false; + } + } + } diff --git a/application/controllers/ProfileController.php b/application/controllers/ProfileController.php new file mode 100644 index 0000000..e2a7801 --- /dev/null +++ b/application/controllers/ProfileController.php @@ -0,0 +1,380 @@ +width = $width; + $this->height = $height; + $this->margint = $margint; + $this->marginr = $marginr; + $this->marginb = $marginb; + $this->marginl = $marginl; + + $this->minx = 0; + $this->miny = 0; + $this->maxx = $this->width; + $this->maxy = $this->height; + + $this->picture = imagecreatetruecolor ($this->width, $this->height); + } + + function __destruct() { + if ($this->picture) { + imagedestroy ($this->picture); + } + } + + function setViewBox($minx, $miny, $maxx, $maxy) { + $this->xdelta = pow(10, floor(log10($maxx))); + $this->maxx = ceil ($maxx / $this->xdelta) * $this->xdelta; + $this->minx = floor ($minx / $this->xdelta) * $this->xdelta; + + $this->ydelta = pow(10, floor(log10($maxy))); + $this->maxy = ceil ($maxy / $this->ydelta) * $this->ydelta; + $this->miny = floor ($miny / $this->ydelta) * $this->ydelta; + + return $this; + } + + function getViewBox() { + return array($this->minx, $this->miny, $this->maxx, $this->maxy); + } + + function setThickness($thickness) { + imagesetthickness ($this->picture, $thickness); + + return $this; + } + + function setFont($fontfile, $fontsize) { + $this->fontfile = $fontfile; + $this->fontsize = $fontsize; + + return $this; + } + + function text($x, $y, $text, $color, $pos = "lt", $limits = array(null, null, null, null)) { + $bbox = imagettfbbox($this->fontsize, 0, $this->fontfile, $text); + + switch (substr($pos, 0, 1)) { + case "r": + $x -= $bbox[2] - $bbox[0]; + break; + case "c": + $x -= ($bbox[2] - $bbox[0]) / 2; + break; + case "l": + default: + break; + } + + switch (substr($pos, 1, 1)) { + case "b": + $y -= $bbox[7] - $bbox[1]; + break; + case "c": + $y -= ($bbox[7] - $bbox[1]) / 2; + break; + case "t": + default: + break; + } + + list($limtop, $limright, $limbottom, $limleft) = $limits; + + if (!is_null($limtop)) { + $y = max($limtop, $y); + } + if (!is_null($limright)) { + $x = min($limright, $x); + } + if (!is_null($limbottom)) { + $y = min($limbottom, $y); + } + if (!is_null($limleft)) { + $x = max($limleft, $x); + } + + imagettftext($this->picture, $this->fontsize, 0, $x, $y, $this->color($color), $this->fontfile, $text); + } + + function rect($x1, $y1, $x2, $y2, $color, $translate = false) { + imagefilledrectangle ($this->picture, $x1, $y1, $x2, $y2, $this->color($color)); + + return $this; + } + + function line($x1, $y1, $x2, $y2, $color) { + imageline ($this->picture, $x1, $y1, $x2, $y2, $this->color($color)); + + return $this; + } + + function draw($filename = null) { + imagepng($this->picture, $filename); + + return $this; + } + + private $colorcache = array(); + function color($str) { + if (!isset ($this->colorcache[$str])) { + $r = "0x" . substr($str, 0, 2); + $g = "0x" . substr($str, 2, 2); + $b = "0x" . substr($str, 4, 2); + $this->colorcache[$str] = imagecolorallocate ($this->picture, $r, $g, $b); + } + return $this->colorcache[$str]; + } + + function canvasXPos($x) { + return round(($x - $this->minx) * ($this->width - $this->marginr - $this->marginl) / ($this->maxx - $this->minx)) + $this->marginl; + } + function canvasYPos($y) { + return $this->height - round(($y - $this->miny) * ($this->height - $this->marginb - $this->margint) / ($this->maxy - $this->miny)) - $this->margint; + } +} + +class ProfileController extends Zend_Controller_Action { + public function init() { + if (!extension_loaded('gd')) { + throw new Syj_Exception_NotImplemented("gd is not installed"); + } + $this->_helper->SyjNoRender->disableRender(); + } + + public function indexAction() { + $url = $this->getRequest()->getUserParam('url'); + if (!isset($url)) { + throw new Syj_Exception_NotFound('Not Found', 404); + } + + $pathMapper = new Syj_Model_PathMapper(); + $path = new Syj_Model_Path(); + if (!$pathMapper->findByUrl($url, $path)) { + if (is_numeric($url) and $pathMapper->hasexisted($url)) { + throw new Syj_Exception_NotFound('Gone', 410); + } else { + throw new Syj_Exception_NotFound('Not Found', 404); + } + } + + $size = $this->getRequest()->getQuery('size', 'big'); + if ($size == 'small') { + $width = 300; + $height = 225; + } else { + $width = 800; + $height = 600; + $size = 'big'; + } + + $file = $path->getProfileCache($size); + if (file_exists($file)) { + if (filesize($file) == 0) { + throw new Syj_Exception_NotImplemented("could not compute altitude profile"); + } + $this->sendFile($file); + return; + } + + try { + $service = $this->_helper->SyjAltiService->service(); + /* we accept 2% of invalid values in the profile */ + $profile = $path->getAltiProfile($service, 2 / 100); + } catch(Syj_Exception_NotImplemented $e) { + @touch($file); + throw $e; + } + + $canvas = $this->drawProfile($profile, $width, $height); + $canvas->draw($file); + $this->sendFile($file); + } + + protected function drawProfile($profile, $width, $height) { + $margint = 50; + $marginr = 50; + $marginb = 50; + $marginl = 50; + + $last = end($profile); + $maxdist = $last[0]; + + $minalti = array(array(0, INF)); + $maxalti = array(array(0, -INF)); + foreach ($profile as $coords) { + $alti = $coords[1]; + + if ($alti == $minalti[0][1]) { + $minalti []= array($coords[0], $alti); + } else if ($alti < $minalti[0][1]) { + $minalti = array(array($coords[0], $alti)); + } + + if ($alti == $maxalti[0][1]) { + $maxalti []= array($coords[0], $alti); + } else if ($alti > $maxalti[0][1]) { + $maxalti = array(array($coords[0], $alti)); + } + } + + $canvas = new Canvas($width, $height, $margint, $marginr, $marginb, $marginl); + $canvas->setViewBox(0, $minalti[0][1], $maxdist, $maxalti[0][1]); + + $canvas->rect(0, 0, $width, $height, "FFFFFF"); + + $slopecolor = function ($slope) { + if ($slope > 18) { + return "FF0000"; // red + } else if ($slope > 12) { + return "FF4000"; + } else if ($slope > 7) { + return "FF8000"; + } else if ($slope > 3) { + return "FFC000"; + } else if ($slope > 0) { + return "FFFF00"; // yellow + } else if ($slope > -3) { + return "00FFFF"; + } else if ($slope > -7) { + return "00C0FF"; + } else if ($slope > -12) { + return "0080FF"; + } else if ($slope > -18) { + return "0040FF"; + } else { + return "0000FF"; // blue; + } + }; + + $canvas->setThickness (2); + + $prev = null; + foreach ($profile as $coord) { + if (!is_null($prev)) { + $dist = $coord[0]; + $alti = $coord[1]; + $prevdist = $prev[0]; + $prevalti = $prev[1]; + + $slope = 100 * ($alti - $prevalti) / ($dist - $prevdist) ; + $canvas->line($canvas->canvasXPos($prevdist), $canvas->canvasYPos($prevalti), + $canvas->canvasXPos($dist), $canvas->canvasYPos($alti), + $slopecolor($slope), true); + } + $prev = $coord; + } + + $canvas->setThickness (1); + $canvas->setFont(APPLICATION_PATH . '/resources/' . 'DejaVuSans.ttf', 10); + + list ($minx, $miny, $maxx, $maxy) = $canvas->getViewBox(); + + $deltax = pow(10, floor(log10($maxx - $minx))); + foreach (range($minx, $maxx, $deltax) as $x) { + $canvas->line($canvas->canvasXPos($x), $height - $marginb + 10, $canvas->canvasXPos($x), $height - $marginb, "000000"); + if ($deltax < 1000) { + $text = $x . "m"; + } else { + $text = round($x / 1000) . "km"; + } + $canvas->text($canvas->canvasXPos($x), $height - $marginb + 10 + 3, $text, "000000", "cb"); + } + $canvas->line($marginl, $height - $marginb, $width - $marginr, $height - $marginb, "000000"); + + $deltay = pow(10, floor(log10($maxy - $miny))); + foreach (range($miny, $maxy, $deltay) as $y) { + $canvas->line($marginl - 10, $canvas->canvasYPos($y), $marginl, $canvas->canvasYPos($y), "000000"); + $text = $y . "m"; + $canvas->text($marginl - 10 - 3, $canvas->canvasYPos($y), $text, "000000", "rc"); + } + $canvas->line($marginl, $height - $marginb, $marginr, $margint, "000000"); + + list($minx, $miny, $maxx, $maxy) = $canvas->getViewBox(); + $limits = array($canvas->canvasYPos($maxy) + 3, $canvas->canvasXPos($maxx) - 3, $canvas->canvasYPos($miny) - 3, $canvas->canvasXPos($minx) + 3); + + $prev = null; + foreach ($minalti as $coords) { + list ($dist, $alti) = $coords; + if (!is_null($prev)) { + $xdist = $canvas->canvasXPos($dist) - $canvas->canvasXPos($prev[0]); + /* second label would be less than 10px from previous one. Do + not display it */ + if ($xdist < 10) { + continue; + } + } + $text = round($alti) . "m"; + $xpos = $canvas->canvasXPos($dist); + $ypos = $canvas->canvasYPos($alti); + $canvas->text($xpos, $ypos + 5, $text, "000000", "cb", $limits); + $canvas->rect($xpos - 1, $ypos - 1, $xpos + 1, $ypos + 1, "000000"); + $prev = $coords; + } + + $prev = null; + foreach ($maxalti as $coords) { + list ($dist, $alti) = $coords; + if (!is_null($prev)) { + $xdist = $canvas->canvasXPos($dist) - $canvas->canvasXPos($prev[0]); + /* second label would be less than 10px from previous one. Do + not display it */ + if ($xdist < 10) { + continue; + } + } + $text = round($alti) . "m"; + $xpos = $canvas->canvasXPos($dist); + $ypos = $canvas->canvasYPos($alti); + $canvas->text($xpos, $ypos - 5, $text, "000000", "ct", $limits); + $canvas->rect($xpos - 1, $ypos - 1, $xpos + 1, $ypos + 1, "000000"); + $prev = $coords; + } + + return $canvas; + } + + protected function sendFile($file) { + $lastmodified = filemtime($file); + + $request = $this->getRequest(); + $response = $this->getResponse(); + + $response->setHeader('Content-Type', 'image/png', true) + ->setHeader('Content-Length', filesize($file), true); + + if ($request->getServer("HTTP_IF_MODIFIED_SINCE")) { + if ($lastmodified <= strtotime($request->getServer("HTTP_IF_MODIFIED_SINCE"))) { + $response->setHttpResponseCode(304); + return; + } + } + + // no-cache is needed otherwise IE does not try to get new version. + $response->setHeader ('Cache-control', 'no-cache, must-revalidate', true) + ->setHeader("Last-Modified", gmdate("D, d M Y H:i:s", filemtime($file)) . " GMT", true); + + readfile($file); + } +} diff --git a/application/controllers/helpers/SyjAltiService.php b/application/controllers/helpers/SyjAltiService.php new file mode 100644 index 0000000..e3d44b3 --- /dev/null +++ b/application/controllers/helpers/SyjAltiService.php @@ -0,0 +1,25 @@ +getParam('altiphp'); + if ($params['source'] == 'srtmtiles' and isset($params['cache'])) { + $cachedir = $params['cache']; + if (!is_dir($cachedir)) { + if (@mkdir($cachedir, 0755, true) === false) { + throw new Zend_Exception(); + } + } + } + self::$_service = new alti\Alti($params); + } + return self::$_service; + } +} diff --git a/application/controllers/helpers/SyjReset.php b/application/controllers/helpers/SyjReset.php index 862b3a6..807f44b 100644 --- a/application/controllers/helpers/SyjReset.php +++ b/application/controllers/helpers/SyjReset.php @@ -6,6 +6,9 @@ class Syj_Controller_Action_Helper_SyjReset extends Zend_Controller_Action_Helpe { public function resetPlaceHolders() { $controller = $this->getActionController(); + if (!$controller->view) { + return; + } $controller->view->jslocales = null; $controller->view->headScript()->exchangeArray(array()); $controller->view->headLink()->exchangeArray(array()); diff --git a/application/controllers/plugins/SyjLoggedUser.php b/application/controllers/plugins/SyjLoggedUser.php index 3bf13ea..a9f9c06 100644 --- a/application/controllers/plugins/SyjLoggedUser.php +++ b/application/controllers/plugins/SyjLoggedUser.php @@ -7,6 +7,9 @@ class Syj_Controllers_Plugins_SyjLoggedUser extends Zend_Controller_Plugin_Abstr public function postDispatch(Zend_Controller_Request_Abstract $request) { $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer'); $view = $viewRenderer->view; + if (!$view) { + return; + } $helper = Zend_Controller_Action_HelperBroker::getStaticHelper('SyjUserManager'); $view->loggedUser = $helper->current(); } diff --git a/application/exception/NotImplemented.php b/application/exception/NotImplemented.php new file mode 100644 index 0000000..58961be --- /dev/null +++ b/application/exception/NotImplemented.php @@ -0,0 +1,7 @@ +_creator_ip; } + /* + * returns an array containing two arrays. First one is list of distance + * from start. Second on is altitude at that point. + * @error: ratio of void we accept + */ + public function getAltiProfile($altiService, $error = 0) { + $points = $altiService->interpolate($this->_geom); + $points = array_map(function($point) { + if ($point instanceof \gisconverter\Geometry) { + return array($point->lon, $point->lat); + } + return $point; + }, $points); + $altitudes = $altiService->altitude($points); + + if (is_null($altitudes)) { + throw new Syj_Exception_NotImplemented("could not compute altitude profile"); + } + + $res = array(array(0, $altitudes[0])); + $prevpoint = $points[0]; + $prevdist = 0; + + $alticount = count($altitudes); + $altinull = 0; + foreach (range(1, count($altitudes) - 1) as $idx) { + $point = $points[$idx]; + $delta = $altiService->vincentyDistance($prevpoint[0], $prevpoint[1], $point[0], $point[1]); + $dist = $prevdist + $delta; + if ($delta == 0) { // we have two similar points side to side + continue; + } + if (is_null($altitudes[$idx])) { + $altinull++; + continue; + } + $prevpoint = $point; + $prevdist = $dist; + $res[] = array($dist, $altitudes[$idx]); + } + + if ($altinull / $alticount > $error) { + throw new Syj_Exception_NotImplemented("too many void in altitude profile"); + } + return $res; + } + + public function getProfileCache($size) { + $cacheDir = Zend_Controller_Front::getInstance()->getParam('profileCache'); + if (is_file($cacheDir) and !is_dir($cacheDir)) { + throw new Zend_Exception(); + } + if (!is_dir($cacheDir)) { + if (@mkdir($cacheDir, 0755, true) === false) { + throw new Zend_Exception(); + } + } + + return sprintf("%s/%s-%s.png", $cacheDir, (string)$this->_id, $size); + } + + public function invalidateCache() { + @unlink($this->getProfileCache('small')); + @unlink($this->getProfileCache('big')); + } + } diff --git a/application/models/PathMapper.php b/application/models/PathMapper.php index 7d514e9..2bbeabb 100644 --- a/application/models/PathMapper.php +++ b/application/models/PathMapper.php @@ -80,6 +80,7 @@ class Syj_Model_PathMapper $path->id = $this->getDbTable()->insert($data); } else { $this->getDbTable()->update($data, array('id = ?' => $id)); + $path->invalidateCache(); } } diff --git a/application/resources/DejaVuSans.ttf b/application/resources/DejaVuSans.ttf new file mode 100644 index 0000000..27cff47 Binary files /dev/null and b/application/resources/DejaVuSans.ttf differ diff --git a/application/views/helpers/FullBaseUrl.php b/application/views/helpers/FullBaseUrl.php new file mode 100644 index 0000000..e69de29 diff --git a/application/views/scripts/idx/index.phtml b/application/views/scripts/idx/index.phtml index 1e34d35..f5b7b67 100644 --- a/application/views/scripts/idx/index.phtml +++ b/application/views/scripts/idx/index.phtml @@ -96,9 +96,19 @@ if ($this->errorMsg) { $this->translate('gpx export'), $this->translate('gpx export')); ?> + + + profileActive) { ?> +
translate("altitude profile")?>
+
path->urlcomp ?: $this->path->id); + $href = $this->baseUrl() . '/profile/' . $urlcomp . '.png'; + $imgsrc = $this->addParamToUrl($href, 'size', 'small', true); + printf('', $href, $imgsrc); + ?>
+ - diff --git a/library/alti.php b/library/alti.php new file mode 120000 index 0000000..11a2f35 --- /dev/null +++ b/library/alti.php @@ -0,0 +1 @@ +altiphp/alti.php \ No newline at end of file diff --git a/library/altiphp b/library/altiphp new file mode 160000 index 0000000..f3de329 --- /dev/null +++ b/library/altiphp @@ -0,0 +1 @@ +Subproject commit f3de32954675440c16f0992aeb79142eb87ba7f2 diff --git a/public/js/syj.js b/public/js/syj.js index 8c48c99..2f3f676 100644 --- a/public/js/syj.js +++ b/public/js/syj.js @@ -674,6 +674,20 @@ var SYJView = { return; } + $('path-profile-content').select('img').each(function(img) { + var src = img.src; + var found = src.match('foo=bar(\\d*)'); + var num; + if (found) { + num = parseInt(found[1], 10); + img.src = src.replace(found[0], 'foo=bar' + (num+1)); + } else if (src.indexOf('?') === -1) { + img.src = img.src + '?foo=bar0'; + } else { + img.src = img.src + '&foo=bar0'; + } + }); + this.messenger.setMessage(SyjStrings.saveSuccess, "success"); SYJDataUi.viewmode(); document.title = $('geom_title').value;