/*
* Copyright (c) 2014 by Gerrit Grunwald
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.hansolo.fx.heatmap;
import javafx.animation.Interpolator;
import javafx.geometry.Point2D;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import java.util.List;
/**
* Created by
* User: hansolo
* Date: 31.12.12
* Time: 07:49
*/
public class SimpleHeatMap {
private static final SnapshotParameters SNAPSHOT_PARAMETERS = new SnapshotParameters();
private ColorMapping colorMapping;
private LinearGradient mappingGradient;
private boolean fadeColors;
private double radius;
private OpacityDistribution opacityDistribution;
private Image eventImage;
private Canvas monochromeCanvas;
private GraphicsContext ctx;
private WritableImage monochromeImage;
private WritableImage heatMap;
private ImageView heatMapView;
// ******************** Constructors **************************************
public SimpleHeatMap(final double WIDTH, final double HEIGHT) {
this(WIDTH, HEIGHT, ColorMapping.LIME_YELLOW_RED);
}
public SimpleHeatMap(final double WIDTH, final double HEIGHT, ColorMapping COLOR_MAPPING) {
this(WIDTH, HEIGHT, COLOR_MAPPING, 15.5);
}
public SimpleHeatMap(final double WIDTH, final double HEIGHT, ColorMapping COLOR_MAPPING, final double EVENT_RADIUS) {
this(WIDTH, HEIGHT, COLOR_MAPPING, EVENT_RADIUS, true);
}
public SimpleHeatMap(final double WIDTH, final double HEIGHT, ColorMapping COLOR_MAPPING, final double EVENT_RADIUS, final boolean FADE_COLORS) {
SNAPSHOT_PARAMETERS.setFill(Color.TRANSPARENT);
colorMapping = COLOR_MAPPING;
mappingGradient = colorMapping.mapping;
fadeColors = FADE_COLORS;
radius = EVENT_RADIUS;
opacityDistribution = OpacityDistribution.CUSTOM;
eventImage = createEventImage(radius, opacityDistribution);
monochromeCanvas = new Canvas(WIDTH, HEIGHT);
ctx = monochromeCanvas.getGraphicsContext2D();
monochromeImage = new WritableImage((int) WIDTH, (int) HEIGHT);
heatMapView = new ImageView(heatMap);
heatMapView.setMouseTransparent(true);
heatMapView.setOpacity(0.5);
}
// ******************** Methods *******************************************
public ImageView getHeatMapImage() {
return heatMapView;
}
public void addEvent(final double X, final double Y, final Image EVENT_IMAGE, final double OFFSET_X, final double OFFSET_Y) {
ctx.drawImage(EVENT_IMAGE, X - OFFSET_X, Y - OFFSET_Y);
updateHeatMap();
}
public void addEvent(final double X, final double Y) {
addEvent(X, Y, eventImage, radius, radius);
}
public void addEvents(final Point2D... EVENTS) {
for (Point2D event : EVENTS) {
ctx.drawImage(eventImage, event.getX() - radius, event.getY() - radius);
}
updateHeatMap();
}
public void addEvents(final List<Point2D> EVENTS) {
for (Point2D event : EVENTS) {
ctx.drawImage(eventImage, event.getX() - radius, event.getY() - radius);
}
updateHeatMap();
}
public void clearHeatMap() {
ctx.clearRect(0, 0, monochromeCanvas.getWidth(), monochromeCanvas.getHeight());
monochromeImage = new WritableImage(monochromeCanvas.widthProperty().intValue(), monochromeCanvas.heightProperty().intValue());
updateHeatMap();
}
public double getHeatMapOpacity() {
return heatMapView.getOpacity();
}
public void setHeatMapOpacity(final double HEAT_MAP_OPACITY) {
double opacity = HEAT_MAP_OPACITY < 0 ? 0 : (HEAT_MAP_OPACITY > 1 ? 1 : HEAT_MAP_OPACITY);
heatMapView.setOpacity(opacity);
}
public ColorMapping getColorMapping() {
return colorMapping;
}
public void setColorMapping(final ColorMapping COLOR_MAPPING) {
colorMapping = COLOR_MAPPING;
mappingGradient = COLOR_MAPPING.mapping;
updateHeatMap();
}
public boolean isFadeColors() {
return fadeColors;
}
public void setFadeColors(final boolean FADE_COLORS) {
fadeColors = FADE_COLORS;
updateHeatMap();
}
public double getEventRadius() {
return radius;
}
public void setEventRadius(final double RADIUS) {
radius = RADIUS < 1 ? 1 : RADIUS;
eventImage = createEventImage(radius, opacityDistribution);
}
public OpacityDistribution getOpacityDistribution() {
return opacityDistribution;
}
public void setOpacityDistribution(final OpacityDistribution OPACITY_DISTRIBUTION) {
opacityDistribution = OPACITY_DISTRIBUTION;
eventImage = createEventImage(radius, opacityDistribution);
}
public void setSize(final double WIDTH, final double HEIGHT) {
monochromeCanvas.setWidth(WIDTH);
monochromeCanvas.setHeight(HEIGHT);
if (WIDTH > 0 && HEIGHT > 0) {
monochromeImage = new WritableImage(monochromeCanvas.widthProperty().intValue(), monochromeCanvas.heightProperty().intValue());
updateHeatMap();
}
}
public Image createEventImage(final double RADIUS, final OpacityDistribution OPACITY_DISTRIBUTION) {
radius = RADIUS < 1 ? 1 : RADIUS;
Stop[] stops = new Stop[11];
for (int i = 0 ; i < 11 ; i++) {
stops[i] = new Stop(i * 0.1, Color.rgb(255, 255, 255, OPACITY_DISTRIBUTION.distribution[i]));
}
int size = (int) (radius * 2);
WritableImage raster = new WritableImage(size, size);
PixelWriter pixelWriter = raster.getPixelWriter();
double maxDistFactor = 1 / radius;
Color pixelColor;
for (int y = 0 ; y < size ; y++) {
for (int x = 0 ; x < size ; x++) {
double distanceX = radius - x;
double distanceY = radius - y;
double distance = Math.sqrt((distanceX * distanceX) + (distanceY * distanceY));
double fraction = maxDistFactor * distance;
for (int i = 0 ; i < 10 ; i++) {
if (Double.compare(fraction, stops[i].getOffset()) >= 0 && Double.compare(fraction, stops[i + 1].getOffset()) <= 0) {
pixelColor = (Color) Interpolator.LINEAR.interpolate(stops[i].getColor(), stops[i + 1].getColor(), (fraction - stops[i].getOffset()) / 0.1);
pixelWriter.setColor(x, y, pixelColor);
break;
}
}
}
}
return raster;
}
private void updateHeatMap() {
monochromeCanvas.snapshot(SNAPSHOT_PARAMETERS, monochromeImage);
heatMap = new WritableImage(monochromeImage.widthProperty().intValue(), monochromeImage.heightProperty().intValue());
PixelWriter pixelWriter = heatMap.getPixelWriter();
PixelReader pixelReader = monochromeImage.getPixelReader();
Color colorFromMonoChromeImage;
double brightness;
Color mappedColor;
for (int y = 0 ; y < monochromeImage.getHeight() ; y++) {
for (int x = 0 ; x < monochromeImage.getWidth(); x++) {
colorFromMonoChromeImage = pixelReader.getColor(x, y);
//brightness = computeLuminance(colorFromMonoChromeImage.getRed(), colorFromMonoChromeImage.getGreen(), colorFromMonoChromeImage.getBlue());
//brightness = computeBrightness(colorFromMonoChromeImage.getRed(), colorFromMonoChromeImage.getGreen(), colorFromMonoChromeImage.getBlue());
brightness = computeBrightnessFast(colorFromMonoChromeImage.getRed(), colorFromMonoChromeImage.getGreen(), colorFromMonoChromeImage.getBlue());
mappedColor = getColorAt(mappingGradient, brightness);
if (fadeColors) {
//pixelWriter.setColor(x, y, Color.color(mappedColor.getRed(), mappedColor.getGreen(), mappedColor.getBlue(), brightness));
pixelWriter.setColor(x, y, Color.color(mappedColor.getRed(), mappedColor.getGreen(), mappedColor.getBlue(), colorFromMonoChromeImage.getOpacity()));
} else {
pixelWriter.setColor(x, y, mappedColor);
}
}
}
heatMapView.setImage(heatMap);
}
private double computeBrightness(final double RED, final double GREEN, final double BLUE) {
return (0.2126 * RED + 0.7152 * GREEN + 0.0722 * BLUE);
}
private double computeBrightnessFast(final double RED, final double GREEN, final double BLUE) {
return ((RED + RED + BLUE + GREEN + GREEN + GREEN) / 6.0);
}
private double computePerceivedBrightness(final double RED, final double GREEN, final double BLUE) {
return ((0.299 * RED) + (0.587 * GREEN) + (0.114 * BLUE));
}
private double computePerceivedBrightnessFast(final double RED, final double GREEN, final double BLUE) {
return ((RED + RED + RED + BLUE + GREEN + GREEN + GREEN + GREEN) * 0.5);
}
private double computeLuminance(final double RED, final double GREEN, final double BLUE) {
return Math.sqrt(0.241 * (RED * RED) + 0.691 * (GREEN * GREEN) + 0.068 * (BLUE * BLUE));
}
private Color getColorAt(final LinearGradient GRADIENT, final double FRACTION) {
double fraction = FRACTION < 0f ? 0f : (FRACTION > 1 ? 1 : FRACTION);
List<Stop> stops = GRADIENT.getStops();
Stop lowerLimit = new Stop(0.0, stops.get(0).getColor());
Stop upperLimit = new Stop(1.0, stops.get(stops.size() - 1).getColor());
for (Stop stop : stops) {
if (Double.compare(stop.getOffset(), fraction) < 0) {
lowerLimit = new Stop(stop.getOffset(), stop.getColor());
} else if (Double.compare(stop.getOffset(), fraction) == 0) {
return stop.getColor();
} else {
upperLimit = new Stop(stop.getOffset(), stop.getColor());
}
}
double interpolationFraction = (fraction - lowerLimit.getOffset()) / (upperLimit.getOffset() - lowerLimit.getOffset());
return (Color) Interpolator.LINEAR.interpolate(lowerLimit.getColor(), upperLimit.getColor(), interpolationFraction);
}
}