/*
* RapidMiner
*
* Copyright (C) 2001-2014 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.new_plotter.configuration;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import com.rapidminer.gui.new_plotter.configuration.LineFormat.LineStyle;
import com.rapidminer.gui.new_plotter.listener.SeriesFormatListener;
import com.rapidminer.gui.new_plotter.listener.events.SeriesFormatChangeEvent;
import com.rapidminer.gui.new_plotter.listener.events.SeriesFormatChangeEvent.SeriesFormatChangeType;
import com.rapidminer.gui.new_plotter.utility.DataStructureUtils;
import com.rapidminer.tools.I18N;
/**
* Defines dot and line format and other format properties for a data series.
*
* @author Marius Helf, Nils Woehler
*/
public class SeriesFormat implements Cloneable {
private static class ItemShapeFactory {
private static final float SHAPE_SIZE = 7.5f;
public static Shape createSquare() {
final float s = SHAPE_SIZE;
float s_d2 = Math.round(s/2.0);
float s_dm2 = s_d2 * 2;
return new Rectangle2D.Float(-s_d2,-s_d2,s_dm2,s_dm2);
}
public static Shape createCircle() {
final float s = SHAPE_SIZE;
float s_d2 = Math.round(s/2.0);
float s_dm2 = s_d2 * 2;
return new Ellipse2D.Float(-s_d2,-s_d2,s_dm2,s_dm2);
}
public static Shape createDiamond() {
final float s = SHAPE_SIZE;
double d = Math.round(Math.sqrt(2*s*s)/2.0);
final GeneralPath p = new GeneralPath();
p.moveTo(0, d);
p.lineTo(d, 0);
p.lineTo(0, -d);
p.lineTo(-d, 0);
p.closePath();
return p;
// final float s = shapeSize;
// final float l = s / 4.0f;
// final float t = s / 4.0f;
//
// final float SQRT2 = (float) Math.pow(2.0, 0.5);
// final GeneralPath p0 = new GeneralPath();
// p0.moveTo(-l - t, -l + t);
// p0.lineTo(-l + t, -l - t);
// p0.lineTo(0.0f, -t * SQRT2);
// p0.lineTo(l - t, -l - t);
// p0.lineTo(l + t, -l + t);
// p0.lineTo(t * SQRT2, 0.0f);
// p0.lineTo(l + t, l - t);
// p0.lineTo(l - t, l + t);
// p0.lineTo(0.0f, t * SQRT2);
// p0.lineTo(-l + t, l + t);
// p0.lineTo(-l - t, l - t);
// p0.lineTo(-t * SQRT2, 0.0f);
// p0.closePath();
// return p0;
}
public static Shape createTriangular() {
double s = SHAPE_SIZE;
int s_2 = (int)Math.round(s/2.0);
int[] xPoints = new int[] { -s_2, 0, s_2 };
int[] yPoints = new int[] { s_2, -s_2, s_2 };
return new Polygon(xPoints, yPoints, xPoints.length);
}
public static Shape createTurnedTriangular() {
double s = SHAPE_SIZE;
int s_2 = (int)Math.round(s/2.0);
int[] xPoints = new int[] { -s_2, 0, s_2 };
int[] yPoints = new int[] { -s_2, s_2, -s_2 };
return new Polygon(xPoints, yPoints, xPoints.length);
}
public static Shape createCrosshairs() {
double x = 0;
double y = 0;
double pointSize = SHAPE_SIZE - 1.0d;
int[] xPoints = new int[] { (int) Math.ceil(x - pointSize / 6.0d), (int) Math.ceil(x + pointSize / 6.0d), (int) Math.ceil(x + pointSize / 6.0d), (int) Math.ceil(x + pointSize / 2.0d), (int) Math.ceil(x + pointSize / 2.0d), (int) Math.ceil(x + pointSize / 6.0d), (int) Math.ceil(x + pointSize / 6.0d), (int) Math.ceil(x - pointSize / 6.0d),
(int) Math.ceil(x - pointSize / 6.0d), (int) Math.ceil(x - pointSize / 2.0d), (int) Math.ceil(x - pointSize / 2.0d), (int) Math.ceil(x - pointSize / 6.0d) };
int[] yPoints = new int[] { (int) Math.ceil(y - pointSize / 2.0d), (int) Math.ceil(y - pointSize / 2.0d), (int) Math.ceil(y - pointSize / 6.0d), (int) Math.ceil(y - pointSize / 6.0d), (int) Math.ceil(y + pointSize / 6.0d), (int) Math.ceil(y + pointSize / 6.0d), (int) Math.ceil(y + pointSize / 2.0d), (int) Math.ceil(y + pointSize / 2.0d),
(int) Math.ceil(y + pointSize / 6.0d), (int) Math.ceil(y + pointSize / 6.0d), (int) Math.ceil(y - pointSize / 6.0d), (int) Math.ceil(y - pointSize / 6.0d) };
return new Polygon(xPoints, yPoints, xPoints.length);
}
public static Shape createHourglass() {
Area area = new Area();
area.add(new Area(createTriangular()));
area.add(new Area(createTurnedTriangular()));
return area;
}
public static Shape createStar() {
int s_2 = (int)Math.round(0.4*SHAPE_SIZE);
AffineTransform t = new AffineTransform();
t.translate(0, s_2);
Area area = new Area();
area.add(new Area(createTriangular()));
area.add(new Area(t.createTransformedShape(createTurnedTriangular())));
return area;
}
}
/**
* Defines styles for dots in the plot.
*
* @author Marius Helf, Nils Woehler
*
*/
public enum ItemShape {
NONE(null,I18N.getGUILabel("plotter.dotstyle.NONE.label")),
CIRCLE(ItemShapeFactory.createCircle(),I18N.getGUILabel("plotter.dotstyle.CIRCLE.label")),
SQUARE(ItemShapeFactory.createSquare(), I18N.getGUILabel("plotter.dotstyle.SQUARE.label")),
DIAMOND(ItemShapeFactory.createDiamond(),I18N.getGUILabel("plotter.dotstyle.DIAMOND.label")),
TRIANGLE(ItemShapeFactory.createTriangular(),I18N.getGUILabel("plotter.dotstyle.TRIANGLE.label")),
TRIANGLE_UPSIDE_DOWN(ItemShapeFactory.createTurnedTriangular(),I18N.getGUILabel("plotter.dotstyle.TRIANGLE_UPSIDE_DOWN.label")),
STAR(ItemShapeFactory.createStar(),I18N.getGUILabel("plotter.dotstyle.STAR.label")),
HOURGLASS(ItemShapeFactory.createHourglass(),I18N.getGUILabel("plotter.dotstyle.HOURGLASS.label")),
CROSSHAIRS(ItemShapeFactory.createCrosshairs(),I18N.getGUILabel("plotter.dotstyle.CROSSHAIRS.label")),
;
private final Shape shape;
private final String name;
private ItemShape(Shape shape,String name) {
this.shape = shape;
this.name = name;
}
public Shape getShape() {
return shape;
}
/**
* @return The display name of the enum value.
*/
public String getName() {
return name;
}
}
/**
* Defines indicator type of the plot.
*
* @author Marius Helf
*
*/
public enum IndicatorType {
NONE(I18N.getGUILabel("plotter.error_indicator.NONE.label")),
BARS(I18N.getGUILabel("plotter.error_indicator.BARS.label")),
BAND(I18N.getGUILabel("plotter.error_indicator.BAND.label")),
DIFFERENCE(I18N.getGUILabel("plotter.error_indicator.DIFFERENCE.label")),
;
private final String name;
private IndicatorType(String name) {
this.name = name;
}
/**
* @return The display name of the enum value.
*/
public String getName() {
return name;
}
}
public enum FillStyle {
NONE(I18N.getGUILabel("plotter.fillstyle.NONE.label")),
SOLID(I18N.getGUILabel("plotter.fillstyle.SOLID.label")),
CROSS_HATCHED(I18N.getGUILabel("plotter.fillstyle.CROSS_HATCHED.label")),
RISING_HATCHED(I18N.getGUILabel("plotter.fillstyle.RISING_HATCHED.label")),
FALLING_HATCHED(I18N.getGUILabel("plotter.fillstyle.FALLING_HATCHED.label")),
VERTICALLY_HATCHED(I18N.getGUILabel("plotter.fillstyle.VERTICALLY_HATCHED.label")),
HORIZONTALLY_HATCHED(I18N.getGUILabel("plotter.fillstyle.HORIZONTALLY_HATCHED.label"));
private final String name;
private FillStyle(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public enum VisualizationType{
LINES_AND_SHAPES(I18N.getGUILabel("plotter.series_type.lines_and_shapes")),
BARS(I18N.getGUILabel("plotter.series_type.bars")),
AREA(I18N.getGUILabel("plotter.series_type.area"));
private final String name;
private VisualizationType(String name) {
this.name = name;
}
/**
* @return The display name of the enum value.
*/
public String getName() {
return name;
}
}
public enum StackingMode {
NONE(I18N.getGUILabel("plotter.stacking_mode.none")),
ABSOLUTE(I18N.getGUILabel("plotter.stacking_mode.absolute")),
RELATIVE(I18N.getGUILabel("plotter.stacking_mode.percentage"));
private final String name;
private StackingMode(String name) {
this.name = name;
}
/**
* @return The display name of the enum value.
*/
public String getName() {
return name;
}
}
private ItemShape itemShape = ItemShape.CIRCLE; // x, square, circle...
private double itemSize = 1.0;
private Color itemColor = Color.GRAY;
private FillStyle areaFillStyle = FillStyle.SOLID; // hatched, solid, ...
private int opacity = 255;
private StackingMode stackingMode = StackingMode.NONE;
private VisualizationType seriesType = VisualizationType.LINES_AND_SHAPES;
private IndicatorType utilityUsage = IndicatorType.NONE;
private LineFormat lineFormat = new LineFormat();
/**
* The other boundary of the filled area e.g. fills only the space between error bars and values
*/
private List<WeakReference<SeriesFormatListener>> changedListenerList = new LinkedList<WeakReference<SeriesFormatListener>>();
public SeriesFormat() {
}
public Paint getAreaFillPaint() {
return getAreaFillPaint(getItemColor());
}
public Paint getAreaFillPaint(Color color) {
// TODO caching
return areaFillPaintFactory(areaFillStyle, color);
}
public LineStyle getLineStyle() {
return lineFormat.getStyle();
}
public void setLineStyle(LineStyle lineStyle) {
if(lineStyle!=getLineStyle()) {
lineFormat.setStyle(lineStyle);
fireChanged(new SeriesFormatChangeEvent(this, lineStyle));
}
}
public ItemShape getItemShape() {
return itemShape;
}
public void setItemShape(ItemShape itemShape) {
if(itemShape!=this.itemShape) {
this.itemShape = itemShape;
fireChanged(new SeriesFormatChangeEvent(this, itemShape));
}
}
public double getItemSize() {
return itemSize;
}
public void setItemSize(double itemSize) {
if(itemSize != this.itemSize) {
this.itemSize = itemSize;
fireChanged(new SeriesFormatChangeEvent(this, itemSize));
}
}
public float getLineWidth() {
return lineFormat.getWidth();
}
public void setLineWidth(float lineWidth) {
if(lineWidth!=lineFormat.getWidth()) {
lineFormat.setWidth(lineWidth);
fireChanged(new SeriesFormatChangeEvent(this, lineWidth));
}
}
/**
* The ready to use item color (including correct opacity).
*/
public Color getItemColor() {
return itemColor;
}
/**
* Sets the item color to itemColor. The alpha value of itemColor is ignored, instead
* the opacity property of this SeriesFormat is applied.
*/
public void setItemColor(Color itemColor) {
if(itemColor != null && (this.itemColor == null || !itemColor.equals(new Color(this.itemColor.getRed(),this.itemColor.getGreen(),this.itemColor.getBlue(), itemColor.getAlpha())))) {
Color opacityItemColor = new Color(itemColor.getRed(),itemColor.getGreen(),itemColor.getBlue(),opacity);
this.itemColor = opacityItemColor;
fireChanged(new SeriesFormatChangeEvent(this, SeriesFormatChangeType.ITEM_COLOR, this.itemColor));
}
}
public Color getLineColor() {
return lineFormat.getColor();
}
public void setLineColor(Color lineColor) {
Color currentColor = lineFormat.getColor();
if(lineColor!= null && (currentColor == null || !lineColor.equals(new Color(currentColor.getRed(),currentColor.getGreen(),currentColor.getBlue(),lineColor.getAlpha())))) {
Color opacityLineColor = new Color(lineColor.getRed(),lineColor.getGreen(),lineColor.getBlue(),opacity);
lineFormat.setColor(opacityLineColor);
fireChanged(new SeriesFormatChangeEvent(this, SeriesFormatChangeType.LINE_COLOR, currentColor));
}
}
public FillStyle getAreaFillStyle() {
return areaFillStyle;
}
public void setAreaFillStyle(FillStyle areaFillStyle) {
if(areaFillStyle != this.areaFillStyle) {
this.areaFillStyle = areaFillStyle;
fireChanged(new SeriesFormatChangeEvent(this, areaFillStyle));
}
}
public int getOpacity() {
return opacity;
}
/**
* Creates a Paint with the given FillStyle, opacity and the Color returned by getItemColor().
*/
private Paint areaFillPaintFactory(FillStyle style, Color color) {
if(areaFillStyle == FillStyle.NONE) {
return new Color(0,0,0,0);
} else if(areaFillStyle == FillStyle.SOLID) {
return color;
} else {
final int s = 10;
BufferedImage bufferedImage = new BufferedImage(s, s, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = bufferedImage.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, // Anti-alias!
RenderingHints.VALUE_ANTIALIAS_ON);
// g2.setColor(new Color(0,0,0,0));
// g2.fillRect(0, 0, 10, 10);
g2.setColor(color);
g2.setStroke(new BasicStroke(s/10.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER));
if (areaFillStyle == FillStyle.FALLING_HATCHED) {
g2.drawLine(0, 0, s, s);
g2.drawLine(0, -s, 2*s, s);
g2.drawLine(-s, 0, s, 2*s);
} else if (areaFillStyle == FillStyle.RISING_HATCHED) {
g2.drawLine(0, s, s, 0);
g2.drawLine(-s, s, s, -s);
g2.drawLine(0, 2*s, 2*s, 0);
} else if (areaFillStyle == FillStyle.CROSS_HATCHED) {
g2.drawLine(0, s, s, 0); // /
g2.drawLine(0, 0, s, s); // \
} else if (areaFillStyle == FillStyle.HORIZONTALLY_HATCHED) {
g2.drawLine(0, s/4, s, s/4); // -
g2.drawLine(0, 3*s/4, s, 3*s/4); // -
} else if (areaFillStyle == FillStyle.VERTICALLY_HATCHED) {
g2.drawLine(s/4, 0, s/4, s); // |
g2.drawLine(3*s/4, 0, 3*s/4, s); // |
} else {
throw new RuntimeException("Illegal area fill style: " + areaFillStyle + ". This cannot happen");
}
// paint with the texturing brush
Rectangle2D rect = new Rectangle2D.Double(0, 0, s, s);
TexturePaint texturePaint = new TexturePaint(bufferedImage, rect);
return texturePaint;
}
}
public void setOpacity(int opacity) {
if(opacity != this.opacity) {
this.itemColor = DataStructureUtils.setColorAlpha(this.itemColor, opacity);
lineFormat.setColor(DataStructureUtils.setColorAlpha(lineFormat.getColor(), opacity));
this.opacity = opacity;
fireChanged(new SeriesFormatChangeEvent(this, opacity));
}
}
public void addChangeListener(SeriesFormatListener listener) {
changedListenerList.add(new WeakReference<SeriesFormatListener>(listener));
}
public void removeChangeListener(SeriesFormatListener listener) {
Iterator<WeakReference<SeriesFormatListener>> it = changedListenerList.iterator();
while (it.hasNext()) {
SeriesFormatListener l = it.next().get();
if (l == null || listener == l) {
it.remove();
}
}
}
private void fireChanged(SeriesFormatChangeEvent e) {
Iterator<WeakReference<SeriesFormatListener>> it = changedListenerList.iterator();
while (it.hasNext()) {
SeriesFormatListener listener = it.next().get();
if (listener == null) {
it.remove();
} else {
listener.seriesFormatChanged(e);
}
}
}
public float getStrokeLength() {
float[] dashArray = lineFormat.getScaledDashArray();
if (dashArray != null) {
float sum = 0;
for (float value : dashArray) {
sum += value;
}
return sum;
} else {
return 0;
}
}
public BasicStroke getStroke() {
return lineFormat.getStroke();
}
public VisualizationType getSeriesType() {
return seriesType;
}
public void setSeriesType(VisualizationType seriesType) {
if (seriesType != this.seriesType) {
this.seriesType = seriesType;
fireChanged(new SeriesFormatChangeEvent(this, seriesType));
}
}
public StackingMode getStackingMode() {
return stackingMode;
}
public void setStackingMode(StackingMode stackingMode) {
if (stackingMode != this.stackingMode) {
this.stackingMode = stackingMode;
fireChanged(new SeriesFormatChangeEvent(this, stackingMode));
}
}
public IndicatorType getUtilityUsage() {
return utilityUsage;
}
public void setUtilityUsage(IndicatorType errorIndicator) {
if (errorIndicator != this.utilityUsage) {
this.utilityUsage = errorIndicator;
fireChanged(new SeriesFormatChangeEvent(this, errorIndicator));
}
}
/**
* Returns true iff the attribute XYZ to which the dimension refers (e.g. color for Color dimension,
* shape for Shape dimension) is to be calculated individually for each item using the XYZProvider of
* dimensionConfig.
* If this function returns false, the respective attribute is set globally for the whole series.
*/
public static boolean calculateIndividualFormatForEachItem(DimensionConfig domainConfig, DimensionConfig dimensionConfig) {
if (dimensionConfig == null) {
return false;
}
if (dimensionConfig.isGrouping()) {
return false;
} else {
if (domainConfig.isGrouping()) {
return false;
} else {
return true;
}
}
}
/**
* Returns true iff the attribute XYZ to which the dimension refers (e.g. color for Color dimension,
* shape for Shape dimension) is to be calculated individually for each series of a value source using the XYZProvider of
* dimensionConfig.
* If this function returns false, the respective attribute is set globally for the whole value source.
*/
public static boolean useSeriesFormatFromDimensionConfig(DimensionConfig domainConfig, DimensionConfig dimensionConfig) {
if (calculateIndividualFormatForEachItem(domainConfig, dimensionConfig)) {
return false;
}
if (dimensionConfig == null) {
return false;
}
if (dimensionConfig.isGrouping()) {
return true;
} else {
return false;
}
}
@Override
public SeriesFormat clone() {
SeriesFormat clone = new SeriesFormat();
clone.areaFillStyle = areaFillStyle;
clone.utilityUsage = utilityUsage;
clone.itemColor = itemColor;
clone.itemShape = itemShape;
clone.itemSize = itemSize;
clone.lineFormat = lineFormat.clone();
clone.opacity = opacity;
clone.seriesType = seriesType;
clone.stackingMode = stackingMode;
return clone;
}
}