Bus Stop density heatmap
[busui.git] / lib / HeatMap.php
blob:a/lib/HeatMap.php -> blob:b/lib/HeatMap.php
<?php <?php
/* /*
*DISCLAIMER *DISCLAIMER
* http://blog.gmapify.fr/create-beautiful-tiled-heat-maps-with-php-and-gd * http://blog.gmapify.fr/create-beautiful-tiled-heat-maps-with-php-and-gd
*THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES *OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, *INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF *USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT *(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES *OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, *INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF *USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT *(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* *
* @author: Olivier G. <olbibigo_AT_gmail_DOT_com> * @author: Olivier G. <olbibigo_AT_gmail_DOT_com>
* @version: 1.0 * @version: 1.0
* @history: * @history:
* 1.0 creation * 1.0 creation
*/ */
define('PI2', 2*M_PI); define('PI2', 2*M_PI);
   
class HeatMapPoint{ class HeatMapPoint{
public $x,$y; public $x,$y;
function __construct($x,$y) { function __construct($x,$y) {
$this->x = $x; $this->x = $x;
$this->y = $y; $this->y = $y;
} }
function __toString() { function __toString() {
return "({$this->x},{$this->y})"; return "({$this->x},{$this->y})";
} }
}//Point }//Point
   
class HeatMap{ class HeatMap{
//TRANSPARENCY //TRANSPARENCY
public static $WITH_ALPHA = 0; public static $WITH_ALPHA = 0;
public static $WITH_TRANSPARENCY = 1; public static $WITH_TRANSPARENCY = 1;
//GRADIENT STYLE //GRADIENT STYLE
public static $GRADIENT_CLASSIC = 'classic'; public static $GRADIENT_CLASSIC = 'classic';
public static $GRADIENT_FIRE = 'fire'; public static $GRADIENT_FIRE = 'fire';
public static $GRADIENT_PGAITCH = 'pgaitch'; public static $GRADIENT_PGAITCH = 'pgaitch';
//GRADIENT MODE (for heatImage) //GRADIENT MODE (for heatImage)
public static $GRADIENT_NO_NEGATE_NO_INTERPOLATE = 0; public static $GRADIENT_NO_NEGATE_NO_INTERPOLATE = 0;
public static $GRADIENT_NO_NEGATE_INTERPOLATE = 1; public static $GRADIENT_NO_NEGATE_INTERPOLATE = 1;
public static $GRADIENT_NEGATE_NO_INTERPOLATE = 2; public static $GRADIENT_NEGATE_NO_INTERPOLATE = 2;
public static $GRADIENT_NEGATE_INTERPOLATE = 3; public static $GRADIENT_NEGATE_INTERPOLATE = 3;
//NOT PROCESSED PIXEL (for heatImage) //NOT PROCESSED PIXEL (for heatImage)
public static $KEEP_VALUE = 0; public static $KEEP_VALUE = 0;
public static $NO_KEEP_VALUE = 1; public static $NO_KEEP_VALUE = 1;
//CONSTRAINTS //CONSTRAINTS
private static $MIN_RADIUS = 2;//in px private static $MIN_RADIUS = 2;//in px
private static $MAX_RADIUS = 50;//in px private static $MAX_RADIUS = 50;//in px
private static $MAX_IMAGE_SIZE = 10000;//in px private static $MAX_IMAGE_SIZE = 10000;//in px
//generate an $image_width by $image_height pixels heatmap image of $points //generate an $image_width by $image_height pixels heatmap image of $points
public static function createImage($data, $image_width, $image_height, $mode=0, $spot_radius = 30, $dimming = 75, $gradient_name = 'classic'){ public static function createImage($data, $image_width, $image_height, $mode=0, $spot_radius = 30, $dimming = 75, $gradient_name = 'classic'){
$_gradient_name = $gradient_name; $_gradient_name = $gradient_name;
if(($_gradient_name != self::$GRADIENT_CLASSIC) && ($_gradient_name != self::$GRADIENT_FIRE) && ($_gradient_name != self::$GRADIENT_PGAITCH)){ if(($_gradient_name != self::$GRADIENT_CLASSIC) && ($_gradient_name != self::$GRADIENT_FIRE) && ($_gradient_name != self::$GRADIENT_PGAITCH)){
$_gradient_name = self::$GRADIENT_CLASSIC; $_gradient_name = self::$GRADIENT_CLASSIC;
} }
$_image_width = min(self::$MAX_IMAGE_SIZE, max(0, intval($image_width))); $_image_width = min(self::$MAX_IMAGE_SIZE, max(0, intval($image_width)));
$_image_height = min(self::$MAX_IMAGE_SIZE, max(0, intval($image_height))); $_image_height = min(self::$MAX_IMAGE_SIZE, max(0, intval($image_height)));
$_spot_radius = min(self::$MAX_RADIUS, max(self::$MIN_RADIUS, intval($spot_radius))); $_spot_radius = min(self::$MAX_RADIUS, max(self::$MIN_RADIUS, intval($spot_radius)));
$_dimming = min(255, max(0, intval($dimming))); $_dimming = min(255, max(0, intval($dimming)));
if(!is_array($data)){ if(!is_array($data)){
return false; return false;
} }
$im = imagecreatetruecolor($_image_width, $_image_height); $im = imagecreatetruecolor($_image_width, $_image_height);
$white = imagecolorallocate($im, 255, 255, 255); $white = imagecolorallocate($im, 255, 255, 255);
imagefill($im, 0, 0, $white); imagefill($im, 0, 0, $white);
if(self::$WITH_ALPHA == $mode){ if(self::$WITH_ALPHA == $mode){
imagealphablending($im, false); imagealphablending($im, false);
imagesavealpha($im,true); imagesavealpha($im,true);
} }
//Step 1: create grayscale image //Step 1: create grayscale image
foreach($data as $datum){ foreach($data as $datum){
if( (is_array($datum) && (count($datum)==1)) || (!is_array($datum) && ('HeatMapPoint' == get_class($datum)))){//Plot points if( (is_array($datum) && (count($datum)==1)) || (!is_array($datum) && ('HeatMapPoint' == get_class($datum)))){//Plot points
if('HeatMapPoint' != get_class($datum)){ if('HeatMapPoint' != get_class($datum)){
$datum = $datum[0]; $datum = $datum[0];
} }
self::_drawCircularGradient($im, $datum->x, $datum->y, $_spot_radius, $_dimming); self::_drawCircularGradient($im, $datum->x, $datum->y, $_spot_radius, $_dimming);
}else if(is_array($datum)){//Draw lines }else if(is_array($datum)){//Draw lines
$length = count($datum)-1; $length = count($datum)-1;
for($i=0; $i < $length; ++$i){//Loop through points for($i=0; $i < $length; ++$i){//Loop through points
//Bresenham's algorithm to plot from from $datum[$i] to $datum[$i+1]; //Bresenham's algorithm to plot from from $datum[$i] to $datum[$i+1];
self::_drawBilinearGradient($im, $datum[$i], $datum[$i+1], $_spot_radius, $_dimming); self::_drawBilinearGradient($im, $datum[$i], $datum[$i+1], $_spot_radius, $_dimming);
} }
} }
} }
//Gaussian filter //Gaussian filter
if($_spot_radius >= 30){ if($_spot_radius >= 30){
imagefilter($im, IMG_FILTER_GAUSSIAN_BLUR); imagefilter($im, IMG_FILTER_GAUSSIAN_BLUR);
} }
//Step 2: create colored image //Step 2: create colored image
if(FALSE === ($grad_rgba = self::_createGradient($im, $mode, $_gradient_name))){ if(FALSE === ($grad_rgba = self::_createGradient($im, $mode, $_gradient_name))){
return FALSE; return FALSE;
} }
$grad_size = count($grad_rgba); $grad_size = count($grad_rgba);
for($x=0; $x <$_image_width; ++$x){ for($x=0; $x <$_image_width; ++$x){
for($y=0; $y <$_image_height; ++$y){ for($y=0; $y <$_image_height; ++$y){
$level = imagecolorat($im, $x, $y) & 0xFF; $level = imagecolorat($im, $x, $y) & 0xFF;
if( ($level >= 0) && ($level < $grad_size) ){ if( ($level >= 0) && ($level < $grad_size) ){
imagesetpixel($im, $x, $y, $grad_rgba[imagecolorat($im, $x, $y) & 0xFF]); imagesetpixel($im, $x, $y, $grad_rgba[imagecolorat($im, $x, $y) & 0xFF]);
} }
} }
} }
if(self::$WITH_TRANSPARENCY == $mode){ if(self::$WITH_TRANSPARENCY == $mode){
imagecolortransparent($im, $grad_rgba[count($grad_rgba)-1]); imagecolortransparent($im, $grad_rgba[count($grad_rgba)-1]);
} }
return $im; return $im;
}//createImage }//createImage
   
//Heat an image //Heat an image
public static function heatImage($filepath, $gradient_name = 'classic', $mode= 0, $min_level=0, $max_level=255, $gradient_interpolate=0, $keep_value=0){ public static function heatImage($filepath, $gradient_name = 'classic', $mode= 0, $min_level=0, $max_level=255, $gradient_interpolate=0, $keep_value=0){
$_gradient_name = $gradient_name; $_gradient_name = $gradient_name;
if(($_gradient_name != self::$GRADIENT_CLASSIC) && ($_gradient_name != self::$GRADIENT_FIRE) && ($_gradient_name != self::$GRADIENT_PGAITCH)){ if(($_gradient_name != self::$GRADIENT_CLASSIC) && ($_gradient_name != self::$GRADIENT_FIRE) && ($_gradient_name != self::$GRADIENT_PGAITCH)){
$_gradient_name = self::$GRADIENT_CLASSIC; $_gradient_name = self::$GRADIENT_CLASSIC;
} }
$_min_level = min(255, max(0, intval($min_level))); $_min_level = min(255, max(0, intval($min_level)));
$_max_level = min(255, max(0, intval($max_level))); $_max_level = min(255, max(0, intval($max_level)));
   
//try opening jpg first then png then gif format //try opening jpg first then png then gif format
if(FALSE === ($im = @imagecreatefromjpeg($filepath))){ if(FALSE === ($im = @imagecreatefromjpeg($filepath))){
if(FALSE === ($im = @imagecreatefrompng($filepath))){ if(FALSE === ($im = @imagecreatefrompng($filepath))){
if(FALSE === ($im = @imagecreatefromgif($filepath))){ if(FALSE === ($im = @imagecreatefromgif($filepath))){
return FALSE; return FALSE;
} }
} }
} }
if(self::$WITH_ALPHA == $mode){ if(self::$WITH_ALPHA == $mode){
imagealphablending($im, false); imagealphablending($im, false);
imagesavealpha($im,true); imagesavealpha($im,true);
} }
$width = imagesx($im); $width = imagesx($im);
$height = imagesy($im); $height = imagesy($im);
if(FALSE === ($grad_rgba = self::_createGradient($im, $mode, $_gradient_name))){ if(FALSE === ($grad_rgba = self::_createGradient($im, $mode, $_gradient_name))){
return FALSE; return FALSE;
} }
//Convert to grayscale //Convert to grayscale
$grad_size = count($grad_rgba); $grad_size = count($grad_rgba);
$level_range = $_max_level - $_min_level; $level_range = $_max_level - $_min_level;
for($x=0; $x <$width; ++$x){ for($x=0; $x <$width; ++$x){
for($y=0; $y <$height; ++$y){ for($y=0; $y <$height; ++$y){
$rgb = imagecolorat($im, $x, $y); $rgb = imagecolorat($im, $x, $y);
$r = ($rgb >> 16) & 0xFF; $r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF; $g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF; $b = $rgb & 0xFF;
$gray_level = Min(255, Max(0, floor(0.33 * $r + 0.5 * $g + 0.16 * $b)));//between 0 and 255 $gray_level = Min(255, Max(0, floor(0.33 * $r + 0.5 * $g + 0.16 * $b)));//between 0 and 255
if( ($gray_level >= $_min_level) && ($gray_level <= $_max_level) ){ if( ($gray_level >= $_min_level) && ($gray_level <= $_max_level) ){
switch($gradient_interpolate){ switch($gradient_interpolate){
case self::$GRADIENT_NO_NEGATE_NO_INTERPOLATE: case self::$GRADIENT_NO_NEGATE_NO_INTERPOLATE:
//$_max_level takes related lowest gradient color //$_max_level takes related lowest gradient color
//$_min_level takes related highest gradient color //$_min_level takes related highest gradient color
$value = 255 - $gray_level; $value = 255 - $gray_level;
break; break;
case self::$GRADIENT_NEGATE_NO_INTERPOLATE: case self::$GRADIENT_NEGATE_NO_INTERPOLATE:
//$_max_level takes related highest gradient color //$_max_level takes related highest gradient color
//$_min_level takes related lowest gradient color //$_min_level takes related lowest gradient color
$value = $gray_level; $value = $gray_level;
break; break;
case self::$GRADIENT_NO_NEGATE_INTERPOLATE: case self::$GRADIENT_NO_NEGATE_INTERPOLATE:
//$_max_level takes lowest gradient color //$_max_level takes lowest gradient color
//$_min_level takes highest gradient color //$_min_level takes highest gradient color
$value = 255- floor(($gray_level - $_min_level) * $grad_size / $level_range); $value = 255- floor(($gray_level - $_min_level) * $grad_size / $level_range);
break; break;
case self::$GRADIENT_NEGATE_INTERPOLATE: case self::$GRADIENT_NEGATE_INTERPOLATE:
//$_max_level takes highest gradient color //$_max_level takes highest gradient color
//$_min_level takes lowest gradient color //$_min_level takes lowest gradient color
$value = floor(($gray_level - $_min_level) * $grad_size / $level_range); $value = floor(($gray_level - $_min_level) * $grad_size / $level_range);
break; break;
default: default:
} }
imagesetpixel($im, $x, $y, $grad_rgba[$value]); imagesetpixel($im, $x, $y, $grad_rgba[$value]);
}else{ }else{
if(self::$KEEP_VALUE == $keep_value){ if(self::$KEEP_VALUE == $keep_value){
//Do nothing //Do nothing
}else{//self::$NO_KEEP_VALUE }else{//self::$NO_KEEP_VALUE
imagesetpixel($im, $x, $y, imagecolorallocatealpha($im,0,0,0,0)); imagesetpixel($im, $x, $y, imagecolorallocatealpha($im,0,0,0,0));
} }
} }
} }
} }
if(self::$WITH_TRANSPARENCY == $mode){ if(self::$WITH_TRANSPARENCY == $mode){
imagecolortransparent($im, $grad_rgba[count($grad_rgba)-1]); imagecolortransparent($im, $grad_rgba[count($grad_rgba)-1]);
} }
return $im; return $im;
}//heatImage }//heatImage
private static function _drawCircularGradient(&$im, $center_x, $center_y, $spot_radius, $dimming){ private static function _drawCircularGradient(&$im, $center_x, $center_y, $spot_radius, $dimming){
$dirty = array(); $dirty = array();
$ratio = (255 - $dimming) / $spot_radius; $ratio = (255 - $dimming) / $spot_radius;
for($r=$spot_radius; $r > 0; --$r){ for($r=$spot_radius; $r > 0; --$r){
$channel = $dimming + $r * $ratio; $channel = $dimming + $r * $ratio;
$angle_step = 0.45/$r; //0.01; $angle_step = 0.45/$r; //0.01;
//Process pixel by pixel to draw a radial grayscale radient //Process pixel by pixel to draw a radial grayscale radient
for($angle=0; $angle <= PI2; $angle += $angle_step){ for($angle=0; $angle <= PI2; $angle += $angle_step){
$x = floor($center_x + $r*cos($angle)); $x = floor($center_x + $r*cos($angle));
$y = floor($center_y + $r*sin($angle)); $y = floor($center_y + $r*sin($angle));
if(!isset($dirty[$x][$y])){ if(!isset($dirty[$x][$y])){
$previous_channel = @imagecolorat($im, $x, $y) & 0xFF;//grayscale so same value $previous_channel = @imagecolorat($im, $x, $y) & 0xFF;//grayscale so same value
$new_channel = Max(0, Min(255,($previous_channel * $channel)/255)); $new_channel = Max(0, Min(255,($previous_channel * $channel)/255));
imagesetpixel($im, $x, $y, imagecolorallocate($im, $new_channel, $new_channel, $new_channel)); imagesetpixel($im, $x, $y, imagecolorallocate($im, $new_channel, $new_channel, $new_channel));
$dirty[$x][$y] = 0; $dirty[$x][$y] = 0;
} }
} }
} }
}//_drawCircularGradient }//_drawCircularGradient
private static function _drawBilinearGradient(&$im, $point0, $point1, $spot_radius, $dimming){ private static function _drawBilinearGradient(&$im, $point0, $point1, $spot_radius, $dimming){
if($point0->x < $point1->x){ if($point0->x < $point1->x){
$x0 = $point0->x; $x0 = $point0->x;
$y0 = $point0->y; $y0 = $point0->y;
$x1 = $point1->x; $x1 = $point1->x;
$y1 = $point1->y; $y1 = $point1->y;
}else{ }else{
$x0 = $point1->x; $x0 = $point1->x;
$y0 = $point1->y; $y0 = $point1->y;
$x1 = $point0->x; $x1 = $point0->x;
$y1 = $point0->y; $y1 = $point0->y;
} }
   
if( ($x0==$x1) && ($y0==$y1)){//check if same coordinates if( ($x0==$x1) && ($y0==$y1)){//check if same coordinates
return false; return false;
} }
$steep = (abs($y1 - $y0) > abs($x1 - $x0))? true: false; $steep = (abs($y1 - $y0) > abs($x1 - $x0))? true: false;
if($steep){ if($steep){
list($x0, $y0) = array($y0, $x0);//swap list($x0, $y0) = array($y0, $x0);//swap
list($x1, $y1) = array($y1, $x1);//swap list($x1, $y1) = array($y1, $x1);//swap
} }
if($x0>$x1){ if($x0>$x1){
list($x0, $x1) = array($x1, $x0);//swap list($x0, $x1) = array($x1, $x0);//swap
list($y0, $y1) = array($y1, $y0);//swap list($y0, $y1) = array($y1, $y0);//swap
} }
$deltax = $x1 - $x0; $deltax = $x1 - $x0;
$deltay = abs($y1 - $y0); $deltay = abs($y1 - $y0);
$error = $deltax / 2; $error = $deltax / 2;
$y = $y0; $y = $y0;
if( $y0 < $y1){ if( $y0 < $y1){
$ystep = 1; $ystep = 1;
}else{ }else{
$ystep = -1; $ystep = -1;
} }
$step = max(1, floor($spot_radius/ 3)); $step = max(1, floor($spot_radius/ 3));
for($x=$x0; $x<=$x1; ++$x){//Loop through x value for($x=$x0; $x<=$x1; ++$x){//Loop through x value
if(0==(($x-$x0) % $step)){ if(0==(($x-$x0) % $step)){
if($steep){ if($steep){
self::_drawCircularGradient(&$im, $y, $x, $spot_radius, $dimming); self::_drawCircularGradient($im, $y, $x, $spot_radius, $dimming);
}else{ }else{
self::_drawCircularGradient(&$im, $x, $y, $spot_radius, $dimming); self::_drawCircularGradient($im, $x, $y, $spot_radius, $dimming);
} }
} }
$error -= $deltay; $error -= $deltay;
if($error<0){ if($error<0){
$y = $y + $ystep; $y = $y + $ystep;
$error = $error + $deltax; $error = $error + $deltax;
} }
} }
}//_drawBilinearGradient }//_drawBilinearGradient
private static function _createGradient($im, $mode, $gradient_name){ private static function _createGradient($im, $mode, $gradient_name){
//create the gradient from an image //create the gradient from an image
if(FALSE === ($grad_im = imagecreatefrompng('gradient/'.$gradient_name.'.png'))){ if(FALSE === ($grad_im = imagecreatefrompng('gradient/'.$gradient_name.'.png'))){
return FALSE; return FALSE;
} }
$width_g = imagesx($grad_im); $width_g = imagesx($grad_im);
$height_g = imagesy($grad_im); $height_g = imagesy($grad_im);
//Get colors along the longest dimension //Get colors along the longest dimension
//Max density is for lower channel value //Max density is for lower channel value
for($y=$height_g-1; $y >= 0 ; --$y){ for($y=$height_g-1; $y >= 0 ; --$y){
$rgb = imagecolorat($grad_im, 1, $y); $rgb = imagecolorat($grad_im, 1, $y);
//Linear function //Linear function
$alpha = Min(127, Max(0, floor(127 - $y/2))); $alpha = Min(127, Max(0, floor(127 - $y/2)));
if(self::$WITH_ALPHA == $mode){ if(self::$WITH_ALPHA == $mode){
$grad_rgba[] = imagecolorallocatealpha($im, ($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF, $alpha); $grad_rgba[] = imagecolorallocatealpha($im, ($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF, $alpha);
}else{ }else{
$grad_rgba[] = imagecolorallocate($im, ($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF); $grad_rgba[] = imagecolorallocate($im, ($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF);
} }
} }
imagedestroy($grad_im); imagedestroy($grad_im);
unset($grad_im); unset($grad_im);
return($grad_rgba); return($grad_rgba);
}//_createGradient }//_createGradient
}//Heatmap }//Heatmap
?> ?>