HeatMapGenerator.java

package sk.iway.iwcm.stat.heat_map;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.LinearGradientPaint;
import java.awt.MultipleGradientPaint;
import java.awt.RadialGradientPaint;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageProducer;
import java.awt.image.LookupOp;
import java.awt.image.LookupTable;
import java.awt.image.RGBImageFilter;
import java.awt.image.Raster;
import java.awt.image.ShortLookupTable;
import java.io.IOException;
import java.util.List;

import javax.imageio.ImageIO;

import sk.iway.iwcm.Logger;
import sk.iway.iwcm.io.IwcmFile;
import sk.iway.iwcm.io.IwcmOutputStream;

/**
 *  HeatMapGenerator.java
 *  
 *  
 *  	Generates a heat map image into a given file, created from data from given clicks.
 *  	Called from {@link HeatMapDB}.
 *  
 *   Creating the image comes in 3 phases:
 *   	1. Create a monochrome black and white image. Black and white is chosen so that multiple circles do NOT override each other. Instead, they add
 *   		up in intensity 
 *   	2. Create a gradient coloring table that maps shades of black and white into an appropriate color
 *   	3. Use gradient transformation filter to create the colored image
 *   	4. Transform black background to transparent black
 *   
 *@Title        webjet7
 *@Company      Interway s.r.o. (www.interway.sk)
 *@Copyright    Interway s.r.o. (c) 2001-2010
 *@author       $Author: marosurbanec $
 *@version      $Revision: 1.3 $
 *@created      Date: 31.5.2010 14:22:02
 *@modified     $Date: 2004/08/16 06:26:11 $
 */
class HeatMapGenerator
{
	private IwcmFile outFile;
	List<Click> clicks;
	private int xLimit;
	private int yLimit;
	private GeneratorSizeAdjustment sizeAdjustment;
	
	public HeatMapGenerator(List<Click> clicks, IwcmFile output)
	{
		if (clicks.size() == 0)
			throw new NoRecordException();
		outFile = output;
		this.clicks = clicks; 
		this.sizeAdjustment = new LogarithmicDecreasingWithSizeAdjustment(this);
	}

	
	public void generate() throws IOException
	{
		BufferedImage heatMap = createImageFromClicks();
		sk.iway.iwcm.Logger.println(HeatMapGenerator.class, "About to write heat map to "+outFile.getAbsolutePath());
		outFile.getParentFile().mkdirs();
		ImageIO.write(heatMap, "png", new IwcmOutputStream(outFile));
	}


	private BufferedImage createImageFromClicks()
	{
		// Jeeff decided the image should be generated using ALL the clicks, not by a statistical sample
		//if (clicks.size() > UPPER_CLICK_COUNT)
		//	pickRandomOnes();
	
		inferOffset();
		return generateImage();
	}

	
	private void inferOffset()
	{
		xLimit = yLimit = 0;
		
		for (Click click : clicks)
		{
			if (click.x > xLimit)
				xLimit = click.x;
			if (click.y > yLimit)
				yLimit = click.y;
		}
	}
	
	
	public BufferedImage generateImage()
	{
		//adjust circle size to the number of clicks. THe bigger the number, the lower the circles radius
		int CIRCLE_SIZE = sizeAdjustment.calculateCircleSize();
		xLimit += CIRCLE_SIZE / 2;
		yLimit += CIRCLE_SIZE / 2;
		
		sk.iway.iwcm.Logger.println(HeatMapGenerator.class, "About to generate image from "+clicks.size()+" clicks");
		sk.iway.iwcm.Logger.println(HeatMapGenerator.class, String.format("Size: [%d, %d]", xLimit, yLimit));
		BufferedImage monochromeImage = new BufferedImage(xLimit, yLimit, BufferedImage.TRANSLUCENT);
		
		
		BufferedImage gradientImage = createGradientImage();
		float ALPHA = .35f;
		LookupTable colorTable = createColorLookupTable(gradientImage, ALPHA);
		LookupOp colorOp = new LookupOp(colorTable, null);
		
		Graphics2D graphics = (Graphics2D)monochromeImage.getGraphics();
		graphics.setColor(Color.WHITE);
		graphics.fillRect(0, 0, xLimit, yLimit);
		BufferedImage circle = createFadedCircleImage(CIRCLE_SIZE);
		//this causes multiple circles to combine with each other, instead of overriding
		graphics.setComposite(BlendComposite.Multiply.derive(0.75f));
		
		for (Click point : clicks)
		{
			double x = ((Integer)point.x).doubleValue();
			double y = ((Integer)point.y).doubleValue();

			int xCoordinate = (int)x - CIRCLE_SIZE/2;
			int yCoordinate = (int)y - CIRCLE_SIZE/2;
			Logger.debug(HeatMapGenerator.class, "drawing circle: x="+x+" y="+y+" size="+CIRCLE_SIZE);
			graphics.drawImage(circle, null, xCoordinate, yCoordinate);
		}
		
		sk.iway.iwcm.Logger.println(HeatMapGenerator.class, "Crayoning image");
		BufferedImage heatMap = colorOp.filter(monochromeImage, null);
		heatMap = makeBlackTransparent(heatMap);
		
		return heatMap;
	}

	private BufferedImage makeBlackTransparent(BufferedImage heatMap)
	{
		sk.iway.iwcm.Logger.println(HeatMapGenerator.class, "Transforming background to alpha");
		ImageFilter filter = new RGBImageFilter() {
			@Override
			public final int filterRGB(int x, int y, int rgb) {
				Color original = new Color(rgb, true);
				if (original.getRed() < 10 && original.getBlue() < 10 && original.getGreen() < 10) {
					// Mark the alpha bits as zero - transparent
					return 0x00FFFFFF & rgb;
				} else {
					// nothing to do
					return rgb;
				}
			}
		};

		ImageProducer ip = new FilteredImageSource(heatMap.getSource(), filter);
      return imageToBufferedImage(Toolkit.getDefaultToolkit().createImage(ip));
	}
	
	 private static BufferedImage imageToBufferedImage(Image image) {
       BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
       Graphics2D g2 = bufferedImage.createGraphics();
       g2.drawImage(image, 0, 0, null);
       g2.dispose();

       return bufferedImage;
   }


	private BufferedImage createGradientImage()
	{
		Color trasnparentCyan = new Color(Color.CYAN.getRed(), Color.CYAN.getGreen(), Color.CYAN.getBlue(), Color.CYAN.getAlpha());
		Color[] colors = new Color[]{Color.RED.darker(), Color.ORANGE, Color.YELLOW, Color.GREEN.darker(), 
					trasnparentCyan, Color.BLUE, new Color(0.0f, 0.0f, 0.2f, 0.1f)};
		float[] fractions = new float[colors.length];
		float step = 1f / colors.length;

		for (int i = 0; i < colors.length; ++i) {
			fractions[i] = i * step;
			Logger.debug(HeatMapGenerator.class, "fractions[i]="+fractions[i]);
		}

		Dimension size = new Dimension(256, 10);
		LinearGradientPaint gradient = new LinearGradientPaint(
					0, 0, size.width, 1, fractions, colors,
					MultipleGradientPaint.CycleMethod.REPEAT);

		BufferedImage gradientImage = new BufferedImage(256, 10, BufferedImage.TRANSLUCENT);
		Graphics2D g = gradientImage.createGraphics();

		g.setPaint(gradient);
		g.fillRect(0, 0, size.width, size.height);
		g.dispose();
		Logger.debug(HeatMapGenerator.class, "Heat map's gradient image ready");
		
		return gradientImage;
	}
	
	private LookupTable createColorLookupTable(BufferedImage im, float alpha)
	{
		int tableSize = 256;
		Raster imageRaster = im.getData();
		double sampleStep = 1D * im.getWidth() / tableSize; // Sample pixels
																				// evenly
		short[][] colorTable = new short[4][tableSize];
		int[] pixel = new int[1]; // Sample pixel
		Color c;
		for (int i = 0; i < tableSize; ++i)
		{
			imageRaster.getDataElements((int) (i * sampleStep), 0, pixel);
			c = new Color(pixel[0]);
			colorTable[0][i] = (short) c.getRed();
			colorTable[1][i] = (short) c.getGreen();
			colorTable[2][i] = (short) c.getBlue();
			colorTable[3][i] = (short) (0xff);
		}
		LookupTable lookupTable = new ShortLookupTable(0, colorTable);
		Logger.debug(HeatMapGenerator.class, "Prepared heat maps' coloring lookup table");
		return lookupTable;
	}
	
	private BufferedImage createFadedCircleImage(int size)
	{
      BufferedImage im = new BufferedImage(size, size, BufferedImage.TRANSLUCENT); 
      float radius = size / 2f;
      
      //this limit will adjust fading to the number of clicks got as input
      float fadeToLimit = sizeAdjustment.calculateMaximumAlphaForOneCircle();
//      fadeToLimit = 1.0f;
      sk.iway.iwcm.Logger.println(HeatMapGenerator.class, "Circle size: "+size);
      sk.iway.iwcm.Logger.println(HeatMapGenerator.class, "Circle fading: "+fadeToLimit);
      RadialGradientPaint gradient = new RadialGradientPaint(
          radius, radius, radius, new float[] { 0f, fadeToLimit},
          new Color[]{Color.BLACK, new Color(1.0f,1.0f,1.0f,1.0f) });

      Graphics2D g = (Graphics2D) im.getGraphics();

      g.setPaint(gradient);
      g.fillRect(0, 0, size, size);

      g.dispose();
      Logger.debug(HeatMapGenerator.class, "Created circle image for heat map");
      
      return im;
  }
}