<?php
/*============================================================
The Brainhandles.com PHP HeatMap Class
by Greg Bulmash
http://www.brainhandles.com/techno-thoughts/the-brainhandles-heatmap-php-class
Copyright © 2011 Greg Bulmash
Licensed for use under Creative Commons 3.0 By
http://creativecommons.org/licenses/by/3.0/
Must be attributed to Greg Bulmash of Brainhandles.com
=============================================================*/
class bhHeatmap
{
private $dataset = array();
private $overlaypath = "";
private $overlayfile = "";
private $imagex = 0;
private $imagey = 0;
private $mainim = null; //placeholder, will become GD image object
private $dotimage = null; //placeholder will become GD image object
private $dotholder = array(); //placeholder will hold dot data for overlays
private $imagespath = "/path/to/dir/with/dot.png";
private $dimsokay = false;
private $arrayokay = false;
private $overlayokay = true;
public function makeMap(){
// after all the data is imported and set, this gets called
// to process it and create the image
if(!$this->allokay()) return false;
// make blank white image with imagex/imagey dimensions;
if(!$this->makeBaseImage()) return false;
//overlay all the dots
foreach ($this->dataset["points"] as $count => $points){
$this->setIntensity($count);
foreach ($points as $notimportant => $point) {
$this->overlayDot($point);
}
}
// save base image
imagejpeg($this->mainim, $this->overlaypath . "bw." . $this->overlayfile, 90);
// colorize and save image
$this->colorize();
}
private function allokay(){
//returns true if the dimensions, overlay path, incoming array, and path to root images are okay.
//throws errors if these data elements have not been properly specified
if(!$this->dimsokay) trigger_error("Dimensons of output image were not properly specified. Heatmap image cannot be generated.",E_USER_ERROR);
if(!$this->arrayokay) trigger_error("The array of data points was not properly imported. Image cannot be generated.",E_USER_ERROR);
if(!$this->overlayokay) trigger_error("A viable path for saving the output image was not properly specified. Heatmap image cannot be generated.",E_USER_ERROR);
if(!file_exists($this->imagespath . "dot.png")) trigger_error("The path to dot.png was not properly specified. Heatmap image cannot be generated.",E_USER_ERROR);
return true;
}
public function setDims($x,$y){
// Set the dimensions of the output image in pixels.
// Must be bigger than 50 x 50, but currently no upper limit.
// Of course, the bigger the image, the bigger the memory and CPU use.
$this->dimsokay = false;
$isx = false;
$isy = false;
if(intval($x)>50){
$this->imagex = intval($x);
$isx = true;
}
if(intval($y)>50){
$this->imagey = intval($y);
$isy = true;
}
if(($isx)&&($isy)) $this->dimsokay = true;
}
public function setImagesPath($path){
// A dummy value is set in the object initialization.
// You can hardcode the value above and never call this function
// or you can pass the value from your script. The reason not to
// hardcode it would be if you want to use this class with multiple
// color schemes or dot shapes.
if(!is_readable($path)) return false;
if(!file_exists($path . "dot.png")) return false;
$this->imagespath = $path;
}
public function setDimsFromPath($path){
// check that path resolves to a local image and set imagex, imagey for dimensions
$this->dimsokay = false;
$info = getimagesize($path);
if(!info) return;
if(($info[0]>50)&&($info[1]>50)){
$this->imagex = $info[0];
$this->imagey = $info[0];
$this->dimsokay = true;
}
}
public function setOverlay($path, $filename){
// set the path and filename for the file you want
// to write the heatmap overlay image to.
$this->overlayokay = false;
// check if directory exists and is writeable
if(!is_writeable($path)) return false;
//check if filename is okay
if(!preg_match("/^[a-zA-Z0-9\-\.]+$/",$filename)) return false;
$this->overlaypath = $path;
$this->overlayfile = $filename;
$this->overlayokay = true;
}
public function importData($datarray){
// does basic validation and processing of a clickstream:
// input is an array of subarrays with the
// subarrays having "x" and "y" integer values. It counts occurrences.
$this->arrayokay = false;
if(gettype($datarray) != "array") return false;
if((!isset($datarray[0]["x"]))||(!isset($datarray[0]["y"]))) return false;
$point = array("x"=>0,"y"=>0,"count"=>0);
$pointset = array(); // reset array to empty
foreach($datarray as $num=>$points){
// first ensure the point is within the boundaries of the image dimensions
if(intval($points["x"]) < -14) continue;
if(intval($points["x"]) > ($this->imagex + 14)) continue;
if(intval($points["y"]) < -14) continue;
if(intval($points["y"]) > ($this->imagey + 14)) continue;
// create an entry for the point, if one does not exist, increment the count
$wholepoint = intval($points["x"])."x".intval($points["y"]);
if(!isset($pointset[$wholepoint])) {
$pointset[$wholepoint] = $point;
$pointset[$wholepoint]["x"] = intval($points["x"]);
$pointset[$wholepoint]["y"] = intval($points["y"]);
}
$pointset[$wholepoint]["count"] += 1;
}
//Get max & min for intensity adjustments
$this->generateMinMax($pointset);
$this->arrayokay = true;
return true;
}
public function setArray($datarray){
/* if you've already got your points and their relative values
you structure an array like this....
$array["20x40"]["x"] = 20;
$array["20x40"]["y"] = 40;
$array["20x40"]["count"] = 18;
*/
// validate the array
$this->arrayokay = false;
$arrayisgood = true;
foreach($datarray as $point=>$values){
// make sure $point is in the form of "20x40"
if(!preg_match("/^[0-9]+x[0-9]+$/",$point)){
$arrayisgood = false;
break;
}
// make sure $values contains x, y, and count values
if((!isset($values["x"]))||(!isset($values["y"]))||(!isset($values["count"]))){
$arrayisgood = false;
break;
}
// make sure the x, y, and count values are greater than 0
if((!intval($values["x"])>0)||(!intval($values["y"])>0)||(!intval($values["count"])>0)){
$arrayisgood = false;
break;
}
}
if($arrayisgood) {
$this->arrayokay = true;
} else {
return false;
}
$this->generateMinMax($datarray);
}
private function generateMinMax($datarray) {
$this->dataset = array("min"=>8000000000,"max"=>-8000000000,"points"=>array());
foreach($datarray as $point => $info){
$count = $info["count"];
if($this->dataset["min"] > $count) $this->dataset["min"] = $count;
if($this->dataset["max"] < $count) $this->dataset["max"] = $count;
//after determining if the incidence of the point should expand the min or max, we group the points by incidence in $dataset["points"]
if(!isset($this->dataset["points"][$count])) $this->dataset["points"][$count] = array();
$this->dataset["points"][$count][] = array("x"=>$info["x"],"y"=>$info["y"]);
}
}
private function setIntensity($density){
$top = $this->dataset["max"];
$bottom = $this->dataset["min"];
$range = 1 + ($top - $bottom);
//range is (max - min) + 1 to get the true number of points in the range;
$rangeplier = $range/55;
if($range < 25) $rangeplier = $rangeplier * (55/($range*2));
$maxintensity = 70 - (58 - ceil(($range - 1)/$rangeplier));
$adjuster = ($maxintensity < 68) ? 67 - $maxintensity : 0;
$realadjuster = ceil(($adjuster + ($adjuster * (1/(($range + 1) - ($density - $bottom)))))/2);
$intensity = $realadjuster + (70 - (58 - ceil(($density - $bottom)/$rangeplier)));
// now make the dot
$im = imagecreatetruecolor(32,32);
$white = imagecolorallocate($im, 255, 255, 255);
imagefill($im,0,0,$white);
$bol = imagecreatefrompng($this->imagespath . 'dot.png');
imagecopymergegray($im, $bol, 0, 0, 0, 0, 32, 32, $intensity);
$dot = array ("r"=>0,"g"=>0,"b"=>0);
for($y = 0; $y < 32; $y++){
$this->dotholder[$y] = array();
for($x = 0; $x < 32; $x++) {
$this->dotholder[$y][$x] = $dot;
$rgb = imagecolorat($im,$x,$y);
$this->dotholder[$y][$x]["r"] = ($rgb >> 16) & 0xFF;
$this->dotholder[$y][$x]["g"] = ($rgb >> 8) & 0xFF;
$this->dotholder[$y][$x]["b"] = $rgb & 0xFF;
}
}
}
private function makeBaseImage(){
//creates a blank white image using the dimensions
if(!$this->dimsokay) return false;
$this->mainim = imagecreatetruecolor($this->imagex,$this->imagey);
$white = imagecolorallocate($this->mainim, 255, 255, 255);
imagefill($this->mainim, 0, 0, $white);
return true;
}
private function overlayDot($points){
$dotsize = 32; // in future we can make this a settable value
$xmax = $this->imagex;
$ymax = $this->imagey;
$topx = $points["x"] - intval($dotsize/2);
$topy = $points["y"] - intval($dotsize/2);
for($y = 0; $y < $dotsize; $y++){
for($x = 0; $x < $dotsize; $x++){
$inx = $topx + $x;
$iny = $topy + $y;
if(($inx < 0)||($iny < 0)) continue;
if(($inx > ($xmax-1))||($iny > ($ymax-1))) continue;
//get the color for the pixel
$rgb = imagecolorat($this->mainim,$inx,$iny);
$rbase = ($rgb >> 16) & 0xFF;
$gbase = ($rgb >> 8) & 0xFF;
$bbase = $rgb & 0xFF;
// let's multiply this pixel by the corresponding dot pixel
$rbase = intval(($rbase * $this->dotholder[$y][$x]["r"])/255);
$gbase = intval(($gbase * $this->dotholder[$y][$x]["g"])/255);
$bbase = intval(($bbase * $this->dotholder[$y][$x]["b"])/255);
if(isset($rebase)) imagecolordeallocate($this->mainim, $rebase);
// deallocate the $rebase color if it was allocated previously
$rebase = imagecolorallocate($this->mainim, $rbase, $gbase, $bbase);
// allocate the $rebase color with the multiplied value
imagesetpixel($this->mainim, $inx, $iny, $rebase);
// set the pixel to the newly multiplied value
}
}
}
private function colorize(){
$palcolors = $this->importColors($this->imagespath . 'colors.png');
$im = imagecreatefromjpeg($this->overlaypath . "bw." . $this->overlayfile);
$height = imagesy($im);
$width = imagesx($im);
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;
$col = 255 - intval(($r + $g + $b)/3);
if(isset($newcol)) imagecolordeallocate($im, $newcol);
$newcol = imagecolorallocate($im, $palcolors[$col]["r"], $palcolors[$col]["g"], $palcolors[$col]["b"]);
imagesetpixel($im, $x, $y, $newcol);
}
}
imagejpeg($im,$this->overlaypath . $this->overlayfile);
}
private function importColors($image){
$dotpallette = array();
$pixel = array("r"=>0,"g"=>0,"b"=>0);
$sim = imagecreatefrompng($image);
for($i = 0; $i < 256; $i++){
$dotpallette[$i] = $pixel;
$rgb = imagecolorat($sim,16,$i);
$dotpallette[$i]["r"] = ($rgb >> 16) & 0xFF;
$dotpallette[$i]["g"] = ($rgb >> 8) & 0xFF;
$dotpallette[$i]["b"] = $rgb & 0xFF;
}
return $dotpallette;
}
}
?> |