/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package utils;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Line2D.Double;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import com.jhlabs.image.GaussianFilter;
import controllers.routes.javascript;
import setups.AppConfig;
/**
* This class can create a heatmap from a given list of points.
* The output will be a picture visualising the occurences of points.
* <p>
* CIRCLEPIC (an image of a circle, from black to transparent)
* <p>
* and
* <p>
* SPECTRUMPIC (a color gradiant
* where the color that represents "most" is at the bottom)
* <p>
* must be in the user.dir in:
* <p>
* heatmap/bolillaT.png
* <p>
* and
* <p>
* heatmap/colors.png
*
* @author sam
*/
public class HeatMap {
/* // TODO memory leak? also, still quite slow. negate image really
* nececary? maybe it is possible to draw it correct from the start...
* also, maybe remember where circles where drawn, so on remap,
* only those areas
* have to be mapped (disadvantage: more memory, also bad code, probably)
*/
/** half of the size of the circle picture. */
private static final int HALFCIRCLEPICSIZE = 32;
/** path to picture of circle which gets more transparent to the outside. */
private static final String CIRCLEPIC = AppConfig.appRootDirectory + "public/images/heatmap/circle_v2.png"; //System.getProperty("user.dir") + File.separator + "heatmap" + File.separator + "bolillaT.png";
private static final String PATHPIC = AppConfig.appRootDirectory + "public/images/heatmap/path.png"; //System.getProperty("user.dir") + File.separator + "heatmap" + File.separator + "bolillaT.png";
private static final String SPECTRUMPIC = AppConfig.appRootDirectory + "public/images/heatmap/colors.png"; //System.getProperty("user.dir") + "/heatmap/colors.png";
/** map to collect and sort points. */
private Map<Integer, List<Point>> map;
private Map<Integer, List<Rectangle>> mapRects;
/** maximum occurance of the same coordinates. */
private int maxOccurance = 1;
/** maximal given x value. */
private int maxXValue;
/** maximal given y value. */
private int maxYValue;
/** name of file over which the heatmap will be laid. */
private String lvlMap;
/** name of file to save heatmap to. */
private String outputFile;
private int linesRadius = 16;
private List<Line2D.Double> lines;
/**
* constructs new instance of HeatMap from given list of points.
* Depending on the amount of points, this may take a while,
* as the points are being sorted.
*
* @param points the list of points
* @param output name of file to store created heatmap in
* @param lvlMap name of file to lay heatmap over
*/
public HeatMap(List<Point> points, String output, String lvlMap) {
outputFile = output;
this.lvlMap = lvlMap;
initMap(points);
}
public HeatMap(String output, String lvlMap) {
outputFile = output;
this.lvlMap = lvlMap;
map = new HashMap<Integer, List<Point>>();
BufferedImage mapPic = loadImage(lvlMap);
maxXValue = mapPic.getWidth();
maxYValue = mapPic.getHeight();
}
/**
* initiate map.
* counts and sorts points and figures out max x and y values as well
* as the maximal amount of points with same coordinates.
* max x and y values will be used for the size of the heatmap.
*
* @param points list of points
*/
private void initMap(List<Point> points) {
map = new HashMap<Integer, List<Point>>();
BufferedImage mapPic = loadImage(lvlMap);
maxXValue = mapPic.getWidth();
maxYValue = mapPic.getHeight();
int pointSize = points.size();
for (int i = 0; i < pointSize; i++) {
Point point = points.get(i);
// add point to right list.
int hash = getkey(point);
if (map.containsKey(hash)) {
List<Point> thisList = map.get(hash);
thisList.add(point);
if (thisList.size() > maxOccurance) {
maxOccurance = thisList.size();
}
// if list did not exist, create new one and add point.
} else {
List<Point> newList = new LinkedList<Point>();
newList.add(point);
map.put(hash, newList);
}
}
}
public void buildData( List<Rectangle> rectangles ) {
mapRects = new HashMap<Integer, List<Rectangle>>();
int rectsSize = rectangles.size();
for (int i = 0; i < rectsSize; i++) {
Rectangle rect = rectangles.get( i );
int hash = getkey(rect);
if (mapRects.containsKey(hash)) {
List<Rectangle> thisList = mapRects.get(hash);
thisList.add(rect);
if (thisList.size() > maxOccurance) {
maxOccurance = thisList.size();
}
} else {
List<Rectangle> newList = new LinkedList<Rectangle>();
newList.add(rect);
mapRects.put(hash, newList);
}
}
return;
}
/**
* creates the heatmap.
*
* @param multiplier calculated opacity of every point will be
* multiplied by this value. This leads to a HeatMap that is easier to read,
* especially when there are not too many points or the points are too
* spread out. Pass 1.0f for original.
*/
public void createHeatMap(float multiplier) {
BufferedImage circle = loadImage(CIRCLEPIC);
BufferedImage heatMap = new BufferedImage(maxXValue, maxYValue, 6);
paintInColor(heatMap, Color.white);
Iterator<List<Point>> iterator = map.values().iterator();
while (iterator.hasNext()) {
List<Point> currentPoints = iterator.next();
// calculate opaqueness based on number of occurences of current point
float opaque = currentPoints.size() / (float) maxOccurance;
// adjust opacity so the heatmap is easier to read
opaque = opaque * multiplier;
if (opaque > 1) {
opaque = 1;
}
Point currentPoint = currentPoints.get(0);
// draw a circle which gets transparent from middle to outside
//(which opaqueness is set to "opaque") at the position specified by the center of the currentPoint
addImage(heatMap, circle, opaque, (currentPoint.x - HALFCIRCLEPICSIZE), (currentPoint.y - HALFCIRCLEPICSIZE));
}
print("done adding points.");
// negate the image
heatMap = negateImage(heatMap);
// remap black/white with color spectrum from white over red, orange, yellow, green to blue
remap(heatMap);
//Smooth some edges
com.jhlabs.image.GaussianFilter filter = new GaussianFilter( (float) (linesRadius * 0.4) );
filter.filter(heatMap, heatMap);
// blend image over lvlMap at opacity 40%
BufferedImage output = loadImage(lvlMap);
addImage(output, heatMap, 0.4f);
//Add Legend
addImage(output, loadImage(SPECTRUMPIC), 0.6f, 10, 30);
Graphics2D g2 = output.createGraphics();
g2.setColor( new Color(0, 0, 0, (short)(255 * 0.4) ) );
g2.setFont(new Font( "Times", Font.BOLD, 24 ));
g2.drawString("0", 65, 30);
g2.drawString(""+maxOccurance, 65, 530);
g2.dispose();
// save image
// saveImage(heatMap, outputFile); //output medium with just the heat
saveImage(output, outputFile);
print("done creating heatmap.");
}
/**
* creates the heatmap.
*
* @param multiplier calculated opacity of every point will be
* multiplied by this value. This leads to a HeatMap that is easier to read,
* especially when there are not too many points or the points are too
* spread out. Pass 1.0f for original.
*/
public void createFoldHeatMap(float multiplier) {
// BufferedImage circle = loadImage(CIRCLEPIC);
BufferedImage heatMap = new BufferedImage(maxXValue, maxYValue, 6);
paintInColor(heatMap, Color.white);
Iterator<List<Rectangle>> iterator = mapRects.values().iterator();
String maxSize = "";
while (iterator.hasNext()) {
List<Rectangle> currentPoints = iterator.next();
// calculate opaqueness based on number of occurences of current point
float opaque = currentPoints.size() / (float) maxOccurance;
// adjust opacity so the heatmap is easier to read
opaque = opaque * multiplier;
if (opaque > 1) {
opaque = 1;
}
Rectangle currentPoint = currentPoints.get(0);
if( currentPoints.size() >= (float) maxOccurance ) {
maxSize = "W: " + currentPoint.width+" H:" + currentPoint.height;
}
// draw a circle which gets transparent from middle to outside
// (which opaqueness is set to "opaque") at the position specified by the center of the currentPoint
addRectangleHeatSpot(heatMap, opaque, currentPoint);
}
print("done adding points.");
// negate the image
heatMap = negateImage(heatMap);
// remap black/white with color spectrum from white over red, orange, yellow, green to blue
remap(heatMap);
com.jhlabs.image.GaussianFilter filter = new GaussianFilter( (float) (linesRadius * 0.4) );
filter.filter(heatMap, heatMap);
// blend image over lvlMap at opacity 40%
BufferedImage output = loadImage(lvlMap);
addImage(output, heatMap, 0.4f);
//Add Legend
addImage(output, loadImage(SPECTRUMPIC), 0.6f, 10, 30);
Graphics2D g2 = output.createGraphics();
g2.setColor( new Color(0, 0, 0, (short)(255 * 0.4) ) );
g2.setFont(new Font( "Times", Font.BOLD, 24 ));
g2.drawString("0", 65, 30);
g2.drawString(""+maxOccurance + "("+maxSize+")", 65, 530);
g2.dispose();
// save image
// saveImage(heatMap, outputFile);
saveImage(output, outputFile);
print("done creating heatmap.");
}
/**
* creates the heatmap based on paths.
*
* @param multiplier calculated opacity of every point will be
* multiplied by this value. This leads to a HeatMap that is easier to read,
* especially when there are not too many points or the points are too
* spread out. Pass 1.0f for original.
*/
public void createLinesHeatMap(float opaque, List<GeneralPath> paths) {
BufferedImage pathPic = loadImage(PATHPIC);
BufferedImage heatMap = new BufferedImage(maxXValue, maxYValue, 6);
paintInColor(heatMap, Color.white);
Iterator<GeneralPath> iterator = paths.iterator();
while (iterator.hasNext()) {
// System.out.println(opaque);
// draw a circle which gets transparent from middle to outside
// (which opaqueness is set to "opaque")
// at the position specified by the center of the currentPoint
addHeatLine(heatMap, pathPic, opaque, iterator.next());
}
print("done adding paths.");
// negate the image
heatMap = negateImage(heatMap);
// remap black/white with color spectrum from white over red, orange,
// yellow, green to blue
remap(heatMap);
com.jhlabs.image.GaussianFilter filter = new GaussianFilter( (float) (linesRadius * 0.8) );
filter.filter(heatMap, heatMap);
// blend image over lvlMap at opacity 40%
BufferedImage output = loadImage(lvlMap);
addImage(output, heatMap, 0.4f);
// save image
// saveImage(heatMap, outputFile);
saveImage(output, outputFile);
print("done creating heatmap.");
}
/**
* remaps black and white picture with colors.
* It uses the colors from SPECTRUMPIC. The whiter a pixel is, the more it
* will get a color from the bottom of it. Black will stay black.
*
* @param heatMapBW black and white heat map
*/
private void remap(BufferedImage heatMapBW) {
BufferedImage colorGradiant = loadImage(SPECTRUMPIC);
int width = heatMapBW.getWidth();
int height = heatMapBW.getHeight();
int gradientHight = colorGradiant.getHeight() - 1;
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
// get heatMapBW color values:
int rGB = heatMapBW.getRGB(i, j);
// calculate multiplier to be applied to height of gradiant.
float multiplier = rGB & 0xff; // blue
multiplier *= ((rGB >>> 8)) & 0xff; // green
multiplier *= (rGB >>> 16) & 0xff; // red
multiplier /= 16581375; // 255f * 255f * 255f
// apply multiplier
int y = (int) (multiplier * gradientHight);
// remap values
// calculate new value based on whitenes of heatMap
// (the whiter, the more a color from the top of colorGradiant
// will be chosen.
int mapedRGB = colorGradiant.getRGB(0, y);
// set new value
heatMapBW.setRGB(i, j, mapedRGB);
}
}
}
/**
* returns a negated version of this image.
*
* @param img buffer to negate
* @return negated buffer
*/
private BufferedImage negateImage(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
int rGB = img.getRGB(x, y);
//Swaps values
//i.e. 255, 255, 255 (white)
//becomes 0, 0, 0 (black)
int r = Math.abs(((rGB >>> 16) & 0xff) - 255); // red inverted
int g = Math.abs(((rGB >>> 8) & 0xff) - 255); // green inverted
int b = Math.abs((rGB & 0xff) - 255); // blue inverted
// transform back to pixel value and set it
img.setRGB(x, y, (r << 16) | (g << 8) | b);
}
}
return img;
}
/**
* changes all pixel in the buffer to the provided color.
*
* @param buff buffer
* @param color color
*/
private void paintInColor(BufferedImage buff, Color color) {
Graphics2D g2 = buff.createGraphics();
g2.setColor(color);
g2.fillRect(0, 0, buff.getWidth(), buff.getHeight());
g2.dispose();
}
/**
* changes the opacity of the image.
*
* @param buff1 buffer to change opacity
* @param opaque new opacity
*/
private void makeTransparent(BufferedImage buff1, float opaque) {
Graphics2D g2d = buff1.createGraphics();
g2d.setComposite(
AlphaComposite.getInstance(AlphaComposite.SRC, opaque));
g2d.drawImage(buff1, 0, 0, null);
g2d.dispose();
}
/**
* prints the contents of buff2 on buff1 with the given opaque value
* starting at position 0, 0.
*
* @param buff1 buffer
* @param buff2 buffer to add to buff1
* @param opaque opacity
*/
private void addImage( BufferedImage buff1, BufferedImage buff2, float opaque) {
addImage(buff1, buff2, opaque, 0, 0);
}
/**
* draws the path on the canvas. uses light transparent colour
* @param buff1
* @param texture
* @param opaque
* @param path
*/
private void addHeatLine(BufferedImage buff1, BufferedImage texture, float opaque, GeneralPath path) {
Short color = (short) (255 * opaque);
float radius = linesRadius;
Color c1 = new Color(255, 255, 255, color);
Color c2 = new Color(0, 0, 0, color);
// GradientPaint paint = new GradientPaint(radius / 2, radius / 2, c2, 0, 0, c2, true);
BasicStroke stroke = new BasicStroke(radius * 2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND );
Graphics2D g2d = (Graphics2D)buff1.getGraphics();// createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opaque));
// g2d.setPaint(paint);
g2d.setColor( c2 );
g2d.setStroke( stroke );
g2d.draw( path );
g2d.dispose();
}
/**
* calculates the direction of the line and draws each line accordingly. Sadly the result wasn't great for some reason ...
* @param buff1
* @param texture
* @param opaque
* @param path
*/
private void addHeatLine(BufferedImage buff1, BufferedImage texture, float opaque, Line2D.Double path) {
Short color = (short) (255 * opaque);
float radius = 16;
Color c1 = new Color(255, 255, 255, color);
Color c2 = new Color(0, 0, 0, color);
java.lang.Double b = Math.abs( path.y2 - path.y1 );
double a = Math.abs( path.x2 - path.x1 );
double degreeB, degreeA;
double c = ( Math.sqrt( Math.pow( a , 2) + Math.pow( b , 2) ) );
// System.out.println("c: " + c);
double sinB = b / c ;
double sinA = a / c ;
// System.out.println( Math.sqrt( Math.pow( a , 2) + Math.pow( b , 2) ) );
// System.out.println( path.y2 + " / " + path.y1 + " / " + path.x2 + " / " + path.x1 + " /b: " + b + " /a: " + a + " /SubB: " + sinB);
degreeB = Math.asin( sinB ) * 180 / Math.PI ;
degreeA = 90 - degreeB;
// System.out.println( "degA: " + degreeA + "degB: " + degreeB );
double diffb = radius;
double diffc = radius / ( sinB );
double diffa = diffc * ( sinA );
// System.out.println( "diffb: " + diffb + " diffa: " + diffa + " diffc: " + diffc + " / y2: " + path.y2 + " /y1: " + path.y1 + " /x2: " + path.x2 + " /x1: " + path.x1+ " /SinB: " + sinB);
double internalX = 0, internalY = 0;
double coef = diffb / diffa;
// if( diffa <= 32 ) {
if( path.x2 > path.x1 ) {
if( path.y2 > path.y1 ) internalX = radius + coef * radius;
else internalX = radius - coef * radius;
} else {
if( path.y2 > path.y1 ) internalX = radius - coef * radius;
else internalX = radius + coef * radius;
}
// System.out.println("InternalX: "+internalX+" , InternalY: " + internalY);
GradientPaint paint = new GradientPaint(radius, radius, c2, (float)internalX, (float)internalY, c1, true);
BasicStroke stroke = new BasicStroke(radius * 2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND );
Graphics2D g2d = (Graphics2D)buff1.getGraphics();// createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opaque));
// g2d.setPaint(textPaint);
g2d.setPaint(paint);
g2d.setStroke( stroke );
g2d.draw( path );
g2d.dispose();
}
private void addRectangleHeatSpot(BufferedImage buff1, float opaque, Rectangle y) {
Short color = (short) (255 * opaque);
Color c1 = new Color(0, 0, 0, color);
Color c2 = new Color(255, 255, 255, color);
Graphics2D g2d = (Graphics2D)buff1.getGraphics();// createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opaque));
g2d.setColor( c1 );
g2d.fill( y );
g2d.dispose();
}
/**
* prints the contents of buff2 on buff1 with the given opaque value.
*
* @param buff1 buffer
* @param buff2 buffer
* @param opaque how opaque the second buffer should be drawn
* @param x x position where the second buffer should be drawn
* @param y y position where the second buffer should be drawn
*/
private void addImage(BufferedImage buff1, BufferedImage buff2, float opaque, int x, int y) {
Graphics2D g2d = buff1.createGraphics();
g2d.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opaque));
g2d.drawImage(buff2, x, y, null);
g2d.dispose();
}
/**
* saves the image in the provided buffer to the destination.
*
* @param buff buffer to be saved
* @param dest destination to save at
*/
private void saveImage(BufferedImage buff, String dest) {
try {
File outputfile = new File(dest);
ImageIO.write(buff, "png", outputfile);
} catch (IOException e) {
print("error saving the image: " + dest + ": " + e);
}
}
/**
* returns a BufferedImage from the Image provided.
*
* @param ref path to image
* @return loaded image
*/
private BufferedImage loadImage(String ref) {
BufferedImage b1 = null;
try {
b1 = ImageIO.read(new File(ref));
} catch (IOException e) {
System.out.println("error loading the image: " + ref + " : " + e);
}
return b1;
}
/**
* returns a hash calculated by the given point.
*
* @param p a point
* @return hash value
*/
private int getkey(Point p) {
return ((p.x << 19) | (p.y << 7));
}
private int getkey(Rectangle p) {
int coordHash = ((int)p.x << 19) | ((int)p.y << 7);
int sizeHash = ((int)p.width << 19) | ((int)p.height << 7);
return (coordHash + sizeHash);
}
/**
* prints string to sto.
*
* @param s string to print
*/
private void print(String s) {
System.out.println(s);
}
}