/*
GanttProject is an opensource project management tool. License: GPL3
Copyright (C) 2012 GanttProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package biz.ganttproject.core.chart.render;
import java.awt.BasicStroke;
import java.awt.Image;
import java.awt.Paint;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.imageio.ImageIO;
import biz.ganttproject.core.chart.canvas.Canvas;
import biz.ganttproject.core.option.ColorOption;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
/**
* Encapsulates style information for rendering graphic primitives. Styles
* are CSS-like and stored in {@link java.util.Properties}. Keys there are composed
* from primitive's style name and specific property name, e.g.
* {@code text.timeline.label.border}. Values roughly correspond to the values of corresponding
* properties in CSS. See docs of the subclasses in this class for more information.
*
* @author Dmitry Barashev
*/
class Style {
final static BasicStroke DEFAULT_STROKE = new BasicStroke();
private static final Map<String, Style> ourCache = Maps.newHashMap();
/**
* Padding which is added between text and border. Property name is 'padding' and
* its value is four space-delimited numbers, which specify padding at top, right, bottom and left
* sides, in this order. Measurement unit is pixel always.
*
* Example: text.foo.padding = 2 2 2 2
*/
static class Padding {
private List<Integer> myValues;
public Padding(Collection<Integer> values) {
myValues = Lists.newArrayList(values);
while (myValues.size() < 4) {
myValues.add(0);
}
}
static Padding parse(String value) {
if (value == null) {
return new Padding(Arrays.asList(0, 0, 0, 0));
}
String[] values = value.trim().split("\\s+");
return new Padding(Collections2.transform(Arrays.asList(values), new Function<String, Integer>() {
@Override
public Integer apply(String input) {
return Integer.valueOf(input);
}
}));
}
int getX() {
return getLeft() + getRight();
}
int getY() {
return getTop() + getBottom();
}
int getTop() {
return myValues.get(0);
}
int getRight() {
return myValues.get(1);
}
int getBottom() {
return myValues.get(2);
}
int getLeft() {
return myValues.get(3);
}
}
private static BasicStroke parseStroke(String[] components) {
boolean solid = true;
for (int i = 0; i < components.length; i++) {
if ("dashed".equalsIgnoreCase(components[i])) {
solid = false;
components[i] = null;
break;
} else if ("solid".equalsIgnoreCase(components[i])) {
solid = true;
components[i] = null;
break;
}
}
int width = 1;
for (int i = 0; i < components.length; i++) {
String s = components[i];
if (s != null && s.endsWith("px")) {
width = Integer.parseInt(s.substring(0, s.length() - 2));
components[i] = null;
}
}
if (solid) {
return new BasicStroke(width);
}
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1f, new float[] { 2.5f }, 0f);
}
/**
* Border style. Property name is 'border' and in the value only color is supported,
* and it should be a 6-digit hex RGB value prefixed with #
* Border is always 1px thick and is drawn at all sides.
*
* Example: text.foo.border = #000000
*/
static class Border {
private final BasicStroke myStroke;
private final java.awt.Color myColor;
Border(java.awt.Color color) {
this(color, DEFAULT_STROKE);
}
Border(java.awt.Color color, BasicStroke stroke) {
myColor = color;
myStroke = stroke;
}
java.awt.Color getColor() {
return myColor;
}
BasicStroke getStroke() {
return myStroke;
}
static Border parse(String value) {
if (value == null) {
return null;
}
String[] components = value.trim().split("\\s+");
BasicStroke stroke = parseStroke(components);
java.awt.Color color = java.awt.Color.BLACK;
for (String s : components) {
if (s != null) {
color = ColorOption.Util.determineColor(s);
break;
}
}
return new Border(color, stroke);
}
}
static class Borders {
private Border myTop;
private Border myLeft;
private Border myRight;
private Border myBottom;
private boolean isHomogeneous = false;
Borders(Border top, Border left, Border bottom, Border right) {
myTop = top;
myLeft = left;
myRight = right;
myBottom = bottom;
}
public Borders(java.awt.Color color) {
this(new Border(color));
}
public Borders(Border border) {
this(border, border, border, border);
isHomogeneous = true;
}
public Border getTop() {
return myTop;
}
public Border getLeft() {
return myLeft;
}
public Border getBottom() {
return myBottom;
}
public Border getRight() {
return myRight;
}
public boolean isHomogeneous() {
return isHomogeneous;
}
public Borders withColor(java.awt.Color color) {
return isHomogeneous ? new Borders(new Border(color, myTop.getStroke())) :
new Borders(
new Border(color, myTop.getStroke()),
new Border(color, myLeft.getStroke()),
new Border(color, myBottom.getStroke()),
new Border(color, myRight.getStroke()));
}
}
static class Color {
private final java.awt.Color myColor;
Color(java.awt.Color color) {
myColor = color;
}
java.awt.Color get() {
return myColor;
}
public static Color parse(String value) {
if (value == null) {
return null;
}
return new Color(ColorOption.Util.determineColor(value));
}
}
static class BackgroundImage {
private static final String BITMAP_PREFIX = "bitmap(";
private static final String URL_PREFIX = "url(";
private final Paint myPaint;
private final Image myImage;
private BackgroundImage(Paint paint, Image image) {
myPaint = paint;
myImage = image;
}
Paint getPaint() {
return myPaint;
}
static BackgroundImage parse(String value) {
if (value == null) {
return null;
}
value = value.trim().toLowerCase();
if (value.startsWith(BITMAP_PREFIX)) {
int closingBrace = value.lastIndexOf(')');
if (closingBrace < 0) {
return null;
}
value = value.substring(BITMAP_PREFIX.length(), closingBrace);
if (value.length() != 16) {
return null;
}
int[] bitmap = new int[16];
for (int i = 0; i < value.length(); i++) {
if (value.charAt(i) == '1') {
bitmap[i] = 1;
}
}
return new BackgroundImage(new ShapePaint(4, 4, bitmap), null);
} else if (value.startsWith(URL_PREFIX)) {
int closingBrace = value.lastIndexOf(')');
if (closingBrace < 0) {
return null;
}
value = value.substring(URL_PREFIX.length(), closingBrace);
URL url = Style.class.getResource(value);
if (url == null) {
return null;
}
try {
BufferedImage image = ImageIO.read(url);
return new BackgroundImage(null, image);
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
enum Visibility {
VISIBLE, HIDDEN
}
private Padding myPadding;
/**
* Foreground color. Property name is 'color' and value is
* a 6-digit hex RGB value prefixed with #
*
* Example: text.foo.color = #ffffff
*/
private Color myColor;
/**
* Background color. Property name is 'background-color' and value is
* a 6-digit hex RGB value prefixed with #
*
* Example: text.foo.background-color = #ffffff
*/
private Color myBackground;
private final Properties myProperties;
private final String myStyleName;
private BackgroundImage myBackgroundImage;
private Borders myBorders;
Style(Properties props, String styleName) {
myProperties = props;
myStyleName = styleName;
myPadding = Padding.parse(props.getProperty(styleName + ".padding"));
myBackground = Color.parse(props.getProperty(styleName + ".background-color"));
Border border = Border.parse(props.getProperty(styleName + ".border"));
Border top = Border.parse(props.getProperty(styleName + ".border-top"));
Border left = Border.parse(props.getProperty(styleName + ".border-left"));
Border bottom = Border.parse(props.getProperty(styleName + ".border-bottom"));
Border right = Border.parse(props.getProperty(styleName + ".border-right"));
if (top == null && left == null && right == null && bottom == null) {
myBorders = (border != null) ? new Borders(border) : null;
} else {
myBorders = new Borders(top == null ? border : top, left == null ? border : left, bottom == null ? border : bottom, right == null ? border : right);
}
myColor = Color.parse(props.getProperty(styleName + ".color"));
}
Padding getPadding() {
return myPadding;
}
Color getForegroundColor(Canvas.Shape shape) {
if (shape.getForegroundColor() != null) {
return new Color(shape.getForegroundColor());
}
return myColor;
}
Color getBackgroundColor(Canvas.Shape primitive) {
if (primitive.getBackgroundColor() != null) {
return new Color(primitive.getBackgroundColor());
}
return myBackground;
}
Paint getBackgroundPaint(Canvas.Rectangle rect) {
if (rect.getBackgroundPaint() != null) {
return rect.getBackgroundPaint();
}
if (myBackgroundImage != null) {
return myBackgroundImage.getPaint();
}
myBackgroundImage = BackgroundImage.parse(myProperties.getProperty(myStyleName + ".background-image"));
return myBackgroundImage == null ? null : myBackgroundImage.getPaint();
}
Image getBackgroundImage() {
if (myBackgroundImage != null) {
return myBackgroundImage.myImage;
}
myBackgroundImage = BackgroundImage.parse(myProperties.getProperty(myStyleName + ".background-image"));
return myBackgroundImage == null ? null : myBackgroundImage.myImage;
}
Borders getBorder(Canvas.Shape shape) {
if ((shape instanceof Canvas.Line || shape instanceof Canvas.Text) && shape.getForegroundColor() != null) {
return myBorders == null ? new Borders(shape.getForegroundColor()) : myBorders.withColor(shape.getForegroundColor());
}
return myBorders;
}
static Style getStyle(Properties props, String styleName) {
Style result = ourCache.get(styleName);
if (result == null) {
result = new Style(props, styleName);
ourCache.put(styleName, result);
}
return result;
}
Visibility getVisibility(Canvas.Shape shape) {
if (!shape.isVisible()) {
return Visibility.HIDDEN;
}
String value = myProperties.getProperty(myStyleName + ".visibility");
if (value == null) {
// ugly hack for Rectangles to let RectangleRenderer report if it consumed a shape or not
return (shape instanceof Canvas.Rectangle) ? Visibility.HIDDEN : Visibility.VISIBLE;
}
try {
return Visibility.valueOf(value.toUpperCase());
} catch (IllegalArgumentException e) {
return Visibility.VISIBLE;
}
}
Float getOpacity(Canvas.Shape shape) {
Float result = shape.getOpacity();
if (result != null) {
return result;
}
String value = myProperties.getProperty(myStyleName + ".opacity");
return (value == null) ? null : Float.valueOf(value);
}
}