Bus Stop density heatmap
[busui.git] / lib / HeatMap.php
blob:a/lib/HeatMap.php -> blob:b/lib/HeatMap.php
--- a/lib/HeatMap.php
+++ b/lib/HeatMap.php
@@ -1,1 +1,275 @@
-
+<?php
+/*
+*DISCLAIMER
+* 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.
+*
+*	@author: Olivier G. <olbibigo_AT_gmail_DOT_com>
+*	@version: 1.0
+*	@history:
+*		1.0	creation
+*/
+	define('PI2', 2*M_PI);
+
+	class HeatMapPoint{
+		public $x,$y;
+		function __construct($x,$y) {
+			$this->x = $x;
+			$this->y = $y;
+		}
+		function __toString() {
+			return "({$this->x},{$this->y})";
+		}
+	}//Point
+
+	class HeatMap{
+		//TRANSPARENCY
+		public static $WITH_ALPHA = 0;
+		public static $WITH_TRANSPARENCY = 1;
+		//GRADIENT STYLE
+		public static $GRADIENT_CLASSIC = 'classic';
+		public static $GRADIENT_FIRE = 'fire';
+		public static $GRADIENT_PGAITCH = 'pgaitch';
+		//GRADIENT MODE (for heatImage)
+		public static $GRADIENT_NO_NEGATE_NO_INTERPOLATE = 0;
+		public static $GRADIENT_NO_NEGATE_INTERPOLATE = 1;
+		public static $GRADIENT_NEGATE_NO_INTERPOLATE = 2;
+		public static $GRADIENT_NEGATE_INTERPOLATE = 3;
+		//NOT PROCESSED PIXEL (for heatImage)
+		public static $KEEP_VALUE = 0;
+		public static $NO_KEEP_VALUE = 1;
+		//CONSTRAINTS
+		private static $MIN_RADIUS = 2;//in px
+		private static $MAX_RADIUS = 50;//in px
+		private static $MAX_IMAGE_SIZE = 10000;//in px
+		
+		//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'){
+			$_gradient_name = $gradient_name;
+			if(($_gradient_name != self::$GRADIENT_CLASSIC) && ($_gradient_name != self::$GRADIENT_FIRE) && ($_gradient_name != self::$GRADIENT_PGAITCH)){
+				$_gradient_name = self::$GRADIENT_CLASSIC;
+			}
+			$_image_width = min(self::$MAX_IMAGE_SIZE, max(0, intval($image_width)));
+			$_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)));
+			$_dimming = min(255, max(0, intval($dimming)));
+			if(!is_array($data)){
+				return false;
+			}
+			$im = imagecreatetruecolor($_image_width, $_image_height);
+			$white = imagecolorallocate($im, 255, 255, 255);
+			imagefill($im, 0, 0, $white);
+			if(self::$WITH_ALPHA == $mode){
+				imagealphablending($im, false);
+				imagesavealpha($im,true);
+			}
+			//Step 1: create grayscale image
+			foreach($data as $datum){
+				if( (is_array($datum) && (count($datum)==1)) || (!is_array($datum) && ('HeatMapPoint' == get_class($datum)))){//Plot points
+					if('HeatMapPoint' != get_class($datum)){
+						$datum = $datum[0];
+					}
+					self::_drawCircularGradient($im, $datum->x, $datum->y, $_spot_radius, $_dimming);
+				}else if(is_array($datum)){//Draw lines
+					$length = count($datum)-1;
+					for($i=0; $i < $length; ++$i){//Loop through points
+						//Bresenham's algorithm to plot from from $datum[$i] to $datum[$i+1];
+						self::_drawBilinearGradient($im, $datum[$i], $datum[$i+1], $_spot_radius, $_dimming);
+					}
+				}
+			}
+			//Gaussian filter
+			if($_spot_radius >= 30){
+				imagefilter($im, IMG_FILTER_GAUSSIAN_BLUR);
+			}
+			//Step 2: create colored image
+			if(FALSE === ($grad_rgba = self::_createGradient($im, $mode, $_gradient_name))){
+				return FALSE;
+			}
+			$grad_size = count($grad_rgba);
+			for($x=0; $x <$_image_width; ++$x){
+				for($y=0; $y <$_image_height; ++$y){
+					$level = imagecolorat($im, $x, $y) & 0xFF;
+					if( ($level >= 0) && ($level < $grad_size) ){
+						imagesetpixel($im, $x, $y, $grad_rgba[imagecolorat($im, $x, $y) & 0xFF]);
+					}
+				}
+			}
+			if(self::$WITH_TRANSPARENCY == $mode){
+				imagecolortransparent($im, $grad_rgba[count($grad_rgba)-1]);
+			}
+			return $im;
+		}//createImage
+
+		//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){
+			$_gradient_name = $gradient_name;
+			if(($_gradient_name != self::$GRADIENT_CLASSIC) && ($_gradient_name != self::$GRADIENT_FIRE) && ($_gradient_name != self::$GRADIENT_PGAITCH)){
+				$_gradient_name = self::$GRADIENT_CLASSIC;
+			}
+			$_min_level = min(255, max(0, intval($min_level)));
+			$_max_level = min(255, max(0, intval($max_level)));
+
+			//try opening jpg first then png then gif format
+			if(FALSE === ($im = @imagecreatefromjpeg($filepath))){
+				if(FALSE === ($im = @imagecreatefrompng($filepath))){
+					if(FALSE === ($im = @imagecreatefromgif($filepath))){
+						return FALSE;
+					}
+				}
+			}
+			if(self::$WITH_ALPHA == $mode){
+				imagealphablending($im, false);
+				imagesavealpha($im,true);
+			}
+			$width = imagesx($im);
+			$height = imagesy($im);	
+			if(FALSE === ($grad_rgba = self::_createGradient($im, $mode, $_gradient_name))){
+				return FALSE;
+			}
+			//Convert to grayscale
+			$grad_size = count($grad_rgba);
+			$level_range = $_max_level - $_min_level;
+			for($x=0; $x <$width; ++$x){
+				for($y=0; $y <$height; ++$y){
+					$rgb = imagecolorat($im, $x, $y);
+					$r = ($rgb >> 16) & 0xFF;
+					$g = ($rgb >> 8) & 0xFF;
+					$b = $rgb & 0xFF;
+					$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) ){
+						switch($gradient_interpolate){
+							case self::$GRADIENT_NO_NEGATE_NO_INTERPOLATE:
+								//$_max_level takes related lowest gradient color
+								//$_min_level takes related highest gradient color
+								$value = 255 - $gray_level;
+								break;
+							case self::$GRADIENT_NEGATE_NO_INTERPOLATE:
+								//$_max_level takes related highest gradient color
+								//$_min_level takes related lowest gradient color
+								$value = $gray_level;
+								break;
+							case self::$GRADIENT_NO_NEGATE_INTERPOLATE:
+								//$_max_level takes lowest gradient color
+								//$_min_level takes highest gradient color
+								$value = 255- floor(($gray_level - $_min_level) * $grad_size / $level_range);
+								break;
+							case self::$GRADIENT_NEGATE_INTERPOLATE:
+								//$_max_level takes highest gradient color
+								//$_min_level takes lowest gradient color
+								$value = floor(($gray_level - $_min_level) * $grad_size / $level_range);
+								break;
+							default:
+						}
+						imagesetpixel($im, $x, $y, $grad_rgba[$value]);
+					}else{
+						if(self::$KEEP_VALUE == $keep_value){
+							//Do nothing
+						}else{//self::$NO_KEEP_VALUE
+							imagesetpixel($im, $x, $y, imagecolorallocatealpha($im,0,0,0,0));
+						}
+					}
+				}
+			}			
+			if(self::$WITH_TRANSPARENCY == $mode){
+				imagecolortransparent($im, $grad_rgba[count($grad_rgba)-1]);
+			}
+			return $im;
+		}//heatImage
+		
+		private static function _drawCircularGradient(&$im, $center_x, $center_y, $spot_radius, $dimming){
+			$dirty = array();
+			$ratio = (255 - $dimming) / $spot_radius;
+			for($r=$spot_radius; $r > 0; --$r){
+				$channel = $dimming + $r * $ratio;
+				$angle_step = 0.45/$r; //0.01;
+				//Process pixel by pixel to draw a radial grayscale radient
+				for($angle=0; $angle <= PI2; $angle += $angle_step){
+					$x = floor($center_x + $r*cos($angle));
+					$y = floor($center_y + $r*sin($angle));
+					if(!isset($dirty[$x][$y])){
+						$previous_channel = @imagecolorat($im, $x, $y) & 0xFF;//grayscale so same value
+						$new_channel = Max(0, Min(255,($previous_channel * $channel)/255));
+						imagesetpixel($im, $x, $y, imagecolorallocate($im, $new_channel, $new_channel, $new_channel));
+						$dirty[$x][$y] = 0;
+					}
+				}
+			}
+		}//_drawCircularGradient
+		
+		private static function _drawBilinearGradient(&$im, $point0, $point1, $spot_radius, $dimming){
+			if($point0->x < $point1->x){
+				$x0 = $point0->x;
+				$y0 = $point0->y;
+				$x1 = $point1->x;
+				$y1 = $point1->y;
+			}else{
+				$x0 = $point1->x;
+				$y0 = $point1->y;
+				$x1 = $point0->x;
+				$y1 = $point0->y;
+			}
+
+			if( ($x0==$x1) && ($y0==$y1)){//check if same coordinates
+				return false;
+			}
+			$steep = (abs($y1 - $y0) > abs($x1 - $x0))? true: false;
+			if($steep){
+				list($x0, $y0) = array($y0, $x0);//swap
+				list($x1, $y1) = array($y1, $x1);//swap
+			}
+			if($x0>$x1){
+				list($x0, $x1) = array($x1, $x0);//swap
+				list($y0, $y1) = array($y1, $y0);//swap
+			}
+			$deltax = $x1 - $x0;
+			$deltay = abs($y1 - $y0);
+			$error = $deltax / 2;
+			$y = $y0;
+			if( $y0 < $y1){
+				$ystep = 1; 
+			}else{
+				$ystep = -1;
+			}
+			$step = max(1, floor($spot_radius/ 3));
+			for($x=$x0; $x<=$x1; ++$x){//Loop through x value
+				if(0==(($x-$x0) % $step)){
+					if($steep){
+						self::_drawCircularGradient(&$im, $y, $x, $spot_radius, $dimming);
+					}else{ 
+						self::_drawCircularGradient(&$im, $x, $y, $spot_radius, $dimming);
+					}
+				}
+				$error -= $deltay;
+				if($error<0){
+						$y = $y + $ystep;
+						$error = $error + $deltax;
+				}
+			}		
+		}//_drawBilinearGradient
+		
+		private static function _createGradient($im, $mode, $gradient_name){
+			//create the gradient from an image
+			if(FALSE === ($grad_im = imagecreatefrompng('gradient/'.$gradient_name.'.png'))){
+				return FALSE;
+			}
+			$width_g = imagesx($grad_im);
+			$height_g = imagesy($grad_im);
+			//Get colors along the longest dimension
+			//Max density is for lower channel value
+			for($y=$height_g-1; $y >= 0 ; --$y){
+					$rgb = imagecolorat($grad_im, 1, $y);
+					//Linear function
+					$alpha = Min(127, Max(0, floor(127 - $y/2)));
+					if(self::$WITH_ALPHA == $mode){
+						$grad_rgba[] = imagecolorallocatealpha($im, ($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF, $alpha);
+					}else{
+						$grad_rgba[] = imagecolorallocate($im, ($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF);
+					}
+			}
+			imagedestroy($grad_im);
+			unset($grad_im);
+			return($grad_rgba);
+		}//_createGradient
+	}//Heatmap
+?>