/*
* Copyright (C) 2014 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* 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, see http://www.gnu.org/licenses/
*/
package com.bc.ceres.swing.figure.support;
import com.bc.ceres.binding.Converter;
import com.bc.ceres.binding.Property;
import com.bc.ceres.binding.PropertyContainer;
import com.bc.ceres.binding.PropertyDescriptor;
import com.bc.ceres.binding.ValueRange;
import com.bc.ceres.binding.accessors.MapEntryAccessor;
import com.bc.ceres.swing.figure.FigureStyle;
import com.bc.ceres.swing.figure.Symbol;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Stroke;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import static java.lang.Math.max;
import static java.lang.Math.min;
public class DefaultFigureStyle extends PropertyContainer implements FigureStyle {
//
// The following property descriptors are SVG/CSS standards (see http://www.w3.org/TR/SVG/styling.html)
//
public static final PropertyDescriptor FILL_COLOR = createFillColorDescriptor();
public static final PropertyDescriptor FILL_OPACITY = createFillOpacityDescriptor();
public static final PropertyDescriptor STROKE_COLOR = createStrokeColorDescriptor();
public static final PropertyDescriptor STROKE_OPACITY = createStrokeOpacityDescriptor();
public static final PropertyDescriptor STROKE_WIDTH = createStrokeWidthDescriptor();
//
// The following property descriptors are not really SVG/CSS standards
//
public static final PropertyDescriptor SYMBOL_NAME = createSymbolNameDescriptor();
public static final PropertyDescriptor SYMBOL_IMAGE = createSymbolImageDescriptor();
public static final PropertyDescriptor SYMBOL_REF_X = createSymbolRefXDescriptor();
public static final PropertyDescriptor SYMBOL_REF_Y = createSymbolRefYDescriptor();
private static final DefaultFigureStyle PROTOTYPE;
private FigureStyle parentStyle;
private String name;
private Map<String, Object> values;
private Stroke stroke;
private Paint strokePaint;
private Paint fillPaint;
private Symbol symbol;
static {
PROTOTYPE = new DefaultFigureStyle();
PROTOTYPE.initPrototypeProperties();
}
public DefaultFigureStyle() {
this("");
}
public DefaultFigureStyle(String name) {
this(name, PROTOTYPE);
}
public DefaultFigureStyle(FigureStyle parentStyle) {
this(null, parentStyle);
}
public DefaultFigureStyle(String name, FigureStyle parentStyle) {
this.name = name != null ? name : (parentStyle != null ? parentStyle.getName() : "");
this.parentStyle = parentStyle != null ? parentStyle : PROTOTYPE;
this.values = new HashMap<String, Object>();
addPropertyChangeListener(new EffectivePropertyNuller());
}
/**
* @deprecated Since Ceres 0.13, use {@link #createFromCss}
*/
@Deprecated
public static FigureStyle createFromCSS(String css) {
return createFromCss(css);
}
public static FigureStyle createFromCss(String css) {
DefaultFigureStyle figureStyle = new DefaultFigureStyle();
figureStyle.fromCssString(css);
return figureStyle;
}
public static FigureStyle createPointStyle(Symbol symbol) {
return createPointStyle(symbol, null, null, null);
}
public static FigureStyle createPointStyle(Symbol symbol, Paint strokePaint, Stroke stroke) {
return createPointStyle(symbol, null, strokePaint, stroke);
}
public static FigureStyle createPointStyle(Symbol symbol, Paint fillPaint, Paint strokePaint, Stroke stroke) {
DefaultFigureStyle figureStyle = setSymbol(symbol);
setStroke(figureStyle, strokePaint, stroke);
setFill(figureStyle, fillPaint);
return figureStyle;
}
public static FigureStyle createLineStyle(Paint strokePaint) {
return createLineStyle(strokePaint, null);
}
public static FigureStyle createLineStyle(Paint strokePaint, Stroke stroke) {
DefaultFigureStyle figureStyle = new DefaultFigureStyle("line-style");
setStroke(figureStyle, strokePaint, stroke);
setFill(figureStyle, null);
return figureStyle;
}
public static DefaultFigureStyle createPolygonStyle(Paint fillPaint) {
return createPolygonStyle(fillPaint, null);
}
public static DefaultFigureStyle createPolygonStyle(Paint fillPaint, Paint strokePaint) {
return createPolygonStyle(fillPaint, strokePaint, null);
}
public static DefaultFigureStyle createPolygonStyle(Paint fillPaint, Paint strokePaint, Stroke stroke) {
DefaultFigureStyle figureStyle = new DefaultFigureStyle("polygon-style");
setFill(figureStyle, fillPaint);
setStroke(figureStyle, strokePaint, stroke);
return figureStyle;
}
@Override
public String getName() {
return name;
}
@Override
public <T> T getValue(String name) {
if (isPropertyDefined(name)) {
return (T) super.getValue(name);
}
Property property = parentStyle.getProperty(name);
if (property != null) {
return (T) property.getValue();
}
return null;
}
@Override
public void setValue(String name, Object value) throws IllegalArgumentException {
if (isPropertyDefined(name)) {
super.setValue(name, value);
return;
}
Property property = parentStyle.getProperty(name);
if (property != null) {
defineProperty(property.getDescriptor(), value);
} else {
// be tolerant, do nothing!
}
}
@Override
public Symbol getSymbol() {
if (symbol == null) {
symbol = getEffectiveSymbol(getSymbolName(), getSymbolImagePath(), getSymbolRefX(), getSymbolRefY());
}
return symbol;
}
@Override
public String getSymbolName() {
return getValue(SYMBOL_NAME.getName(), null);
}
public void setSymbolName(String symbolName) {
setValue(SYMBOL_NAME.getName(), symbolName);
}
@Override
public String getSymbolImagePath() {
return getValue(SYMBOL_IMAGE.getName(), null);
}
public void setSymbolImagePath(String symbolName) {
setValue(SYMBOL_IMAGE.getName(), symbolName);
}
@Override
public double getSymbolRefX() {
return getValue(SYMBOL_REF_X.getName(), 0.0);
}
public void setSymbolRefX(double refX) {
setValue(SYMBOL_REF_X.getName(), refX);
}
@Override
public double getSymbolRefY() {
return getValue(SYMBOL_REF_Y.getName(), 0.0);
}
public void setSymbolRefY(double refY) {
setValue(SYMBOL_REF_Y.getName(), refY);
}
@Override
public Stroke getStroke() {
if (stroke == null) {
stroke = getEffectiveStroke(getStrokeWidth());
}
return stroke;
}
@Override
public Stroke getStroke(double scale) {
Stroke stroke = getStroke();
if (scale != 1.0 && stroke instanceof BasicStroke) {
BasicStroke basicStroke = (BasicStroke) stroke;
return new BasicStroke((float) (basicStroke.getLineWidth() * scale),
basicStroke.getEndCap(),
basicStroke.getLineJoin(),
basicStroke.getMiterLimit(),
basicStroke.getDashArray(),
basicStroke.getDashPhase());
}
return stroke;
}
/**
* Gets the effective stroke paint used for drawing the exterior of a lineal or polygonal shape.
* The effective paint may result from a number of different style properties.
*
* @return The effective stroke paint used for drawing.
*/
@Override
public Paint getStrokePaint() {
if (this.strokePaint == null) {
Color strokeColor = getStrokeColor();
if (strokeColor != null) {
this.strokePaint = getEffectivePaint(strokeColor, getStrokeOpacity());
}
}
return strokePaint;
}
@Override
public Color getStrokeColor() {
Object value = getValue(STROKE_COLOR.getName());
if (value instanceof Color) {
return (Color) value;
} else {
return null;
}
}
public void setStrokeColor(Color strokeColor) {
setValue(STROKE_COLOR.getName(), strokeColor);
}
@Override
public double getStrokeOpacity() {
return getValue(STROKE_OPACITY.getName(), 1.0);
}
public void setStrokeOpacity(double opacity) {
setValue(STROKE_OPACITY.getName(), opacity);
}
@Override
public double getStrokeWidth() {
String name1 = STROKE_WIDTH.getName();
double defaultValue = 0.0;
return getValue(name1, defaultValue);
}
public void setStrokeWidth(double width) {
setValue(STROKE_WIDTH.getName(), width);
}
/**
* Gets the effective fill paint used for drawing the interior of a polygonal shape.
* The effective paint may result from a number of different style properties.
*
* @return The effective fill paint used for drawing.
*/
@Override
public Paint getFillPaint() {
if (fillPaint == null) {
Color fillColor = getFillColor();
if (fillColor != null) {
this.fillPaint = getEffectivePaint(fillColor, getFillOpacity());
}
}
return fillPaint;
}
@Override
public Color getFillColor() {
Object value = getValue(FILL_COLOR.getName());
if (value instanceof Color) {
return (Color) value;
} else {
return null;
}
}
public void setFillColor(Color fillColor) {
setValue(FILL_COLOR.getName(), fillColor);
}
@Override
public double getFillOpacity() {
return getValue(FILL_OPACITY.getName(), 1.0);
}
public void setFillOpacity(double opacity) {
setValue(FILL_OPACITY.getName(), opacity);
}
@Override
public String toCssString() {
Map<String, Property> all = getOrderedPropertyMap();
Set<Map.Entry<String, Property>> entries = all.entrySet();
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Property> entry : entries) {
Property property = entry.getValue();
Object value = property.getValue();
if (value != null) {
if (sb.length() > 0) {
sb.append("; ");
}
sb.append(entry.getKey());
sb.append(":");
sb.append(property.getValueAsText());
}
}
return sb.toString();
}
@Override
public void fromCssString(String css) {
resetAllEffectiveProperties();
StringTokenizer st = new StringTokenizer(css, ";", false);
while (st.hasMoreElements()) {
String token = st.nextToken();
int i = token.indexOf(':');
if (i > 0) {
String name = token.substring(0, i).trim();
String textValue = token.substring(i + 1).trim();
Property property = getProperty(name);
try {
if (property != null) {
property.setValueFromText(textValue);
} else {
property = PROTOTYPE.getProperty(name);
if (property != null) {
Converter<?> converter = property.getDescriptor().getConverter();
if (converter != null) {
Object value = converter.parse(textValue);
defineProperty(property.getDescriptor(), value);
}
}
}
} catch (Exception ignored) {
// be tolerant, do nothing!
// todo - really do nothing or log warning, exception? (nf)
}
}
}
}
private synchronized Map<String, Property> getOrderedPropertyMap() {
// Using a TreeMap makes sure that entries are ordered by key
Map<String, Property> propertyMap = new TreeMap<String, Property>();
if (parentStyle != PROTOTYPE) {
collectProperties(parentStyle.getProperties(), propertyMap);
}
collectProperties(getProperties(), propertyMap);
return propertyMap;
}
private static void collectProperties(Property[] properties, Map<String, Property> propertyMap) {
for (Property property : properties) {
propertyMap.put(property.getName(), property);
}
}
private static DefaultFigureStyle setSymbol(Symbol symbol) {
DefaultFigureStyle figureStyle = new DefaultFigureStyle("point-style");
if (symbol instanceof NamedSymbol) {
NamedSymbol namedSymbol = (NamedSymbol) symbol;
figureStyle.setSymbolName(namedSymbol.getName());
} else if (symbol instanceof ImageSymbol) {
ImageSymbol imageSymbol = (ImageSymbol) symbol;
figureStyle.setSymbolImagePath(imageSymbol.getResourcePath());
figureStyle.setSymbolRefX(imageSymbol.getRefX());
figureStyle.setSymbolRefY(imageSymbol.getRefY());
}
figureStyle.symbol = symbol;
return figureStyle;
}
private static void setFill(DefaultFigureStyle figureStyle, Paint fillPaint) {
if (fillPaint instanceof Color) {
Color fillColor = (Color) fillPaint;
if (fillColor.getAlpha() == 255) {
figureStyle.setFillColor(fillColor);
} else {
figureStyle.setFillColor(new Color(fillColor.getRed(), fillColor.getGreen(), fillColor.getBlue()));
figureStyle.setFillOpacity(getOpacity(fillColor));
}
} else if (fillPaint == null) {
figureStyle.setFillColor(null);
}
figureStyle.fillPaint = fillPaint;
}
private static void setStroke(DefaultFigureStyle figureStyle, Paint strokePaint, Stroke stroke) {
if (strokePaint instanceof Color) {
Color strokeColor = (Color) strokePaint;
if (strokeColor.getAlpha() == 255) {
figureStyle.setStrokeColor(strokeColor);
} else {
figureStyle.setStrokeColor(new Color(strokeColor.getRed(), strokeColor.getGreen(), strokeColor.getBlue()));
figureStyle.setStrokeOpacity(getOpacity(strokeColor));
}
} else if (strokePaint == null) {
figureStyle.setStrokeColor(null);
}
// check for other paints here
if (stroke instanceof BasicStroke) {
BasicStroke basicStroke = (BasicStroke) stroke;
figureStyle.setStrokeWidth(basicStroke.getLineWidth());
// add other stuff here
}
figureStyle.strokePaint = strokePaint;
figureStyle.stroke = stroke;
}
private static double getOpacity(Color strokeColor) {
return Math.round(100.0 / 255.0 * strokeColor.getAlpha()) / 100.0;
}
// Only called once by prototype singleton
private void initPrototypeProperties() {
Field[] declaredFields = DefaultFigureStyle.class.getDeclaredFields();
for (Field declaredField : declaredFields) {
if (declaredField.getType() == PropertyDescriptor.class) {
try {
PropertyDescriptor propertyDescriptor = (PropertyDescriptor) declaredField.get(null);
propertyDescriptor.setDefaultConverter();
if (Color.class.isAssignableFrom(propertyDescriptor.getType())) {
propertyDescriptor.setConverter(new CssColorConverter());
}
defineProperty(propertyDescriptor, propertyDescriptor.getDefaultValue());
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
}
setDefaultValues();
}
private void defineProperty(PropertyDescriptor propertyDescriptor, Object value) {
MapEntryAccessor accessor = new MapEntryAccessor(values, propertyDescriptor.getName());
Property property = new Property(propertyDescriptor, accessor);
addProperty(property);
setValue(property.getName(), value);
}
private Symbol getEffectiveSymbol(String symbolName, String symbolImagePath, double symbolRefX, double symbolRefY) {
if (symbolName != null) {
return NamedSymbol.getSymbol(symbolName);
}
if (symbolImagePath != null) {
return ImageSymbol.createIcon(symbolImagePath, symbolRefX, symbolRefY, this.getClass());
}
return null;
}
private static Paint getEffectivePaint(Color color, double opacity) {
int alpha = min(max((int) (opacity * 255), 0), 255);
if (color.getAlpha() == alpha) {
return color;
}
return new Color(color.getRed(),
color.getGreen(),
color.getBlue(),
alpha);
}
private static Stroke getEffectiveStroke(double width) {
return new BasicStroke((float) width);
}
private void resetAllEffectiveProperties() {
resetEffectiveStrokeProperties();
resetEffectiveFillProperties();
resetEffectiveSymbolProperties();
}
private void resetEffectiveSymbolProperties() {
symbol = null;
}
private void resetEffectiveStrokeProperties() {
stroke = null;
strokePaint = null;
}
private void resetEffectiveFillProperties() {
fillPaint = null;
}
private class EffectivePropertyNuller implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().startsWith("stroke")) {
resetEffectiveStrokeProperties();
} else if (evt.getPropertyName().startsWith("fill")) {
resetEffectiveFillProperties();
} else if (evt.getPropertyName().startsWith("symbol")) {
resetEffectiveSymbolProperties();
}
}
}
private static PropertyDescriptor createFillColorDescriptor() {
return createPropertyDescriptor("fill", Color.class, Color.BLACK, false);
}
private static PropertyDescriptor createFillOpacityDescriptor() {
PropertyDescriptor descriptor = createPropertyDescriptor("fill-opacity", Double.class, 1.0, false);
descriptor.setValueRange(new ValueRange(0.0, 1.0));
return descriptor;
}
private static PropertyDescriptor createStrokeColorDescriptor() {
return createPropertyDescriptor("stroke", Color.class, null, false);
}
private static PropertyDescriptor createStrokeOpacityDescriptor() {
PropertyDescriptor descriptor = createPropertyDescriptor("stroke-opacity", Double.class, 1.0, false);
descriptor.setValueRange(new ValueRange(0.0, 1.0));
return descriptor;
}
private static PropertyDescriptor createStrokeWidthDescriptor() {
PropertyDescriptor descriptor = createPropertyDescriptor("stroke-width", Double.class, 0.0, false);
descriptor.setValueRange(new ValueRange(0.0, Double.POSITIVE_INFINITY));
return descriptor;
}
private static PropertyDescriptor createSymbolNameDescriptor() {
return createPropertyDescriptor("symbol", String.class, null, false);
}
private static PropertyDescriptor createSymbolImageDescriptor() {
return createPropertyDescriptor("symbol-image", String.class, null, false);
}
private static PropertyDescriptor createSymbolRefXDescriptor() {
return createPropertyDescriptor("symbol-ref-x", Double.class, 0.0, false);
}
private static PropertyDescriptor createSymbolRefYDescriptor() {
return createPropertyDescriptor("symbol-ref-y", Double.class, 0.0, false);
}
private static PropertyDescriptor createPropertyDescriptor(String propertyName, Class type, Object defaultValue, boolean notNull) {
PropertyDescriptor descriptor = new PropertyDescriptor(propertyName, type);
descriptor.setDefaultValue(defaultValue);
descriptor.setNotNull(notNull);
return descriptor;
}
private double getValue(String name1, double defaultValue) {
Object value = getValue(name1);
if (value instanceof Number) {
return ((Number) value).doubleValue();
} else {
return defaultValue;
}
}
private String getValue(String name, String defaultValue) {
Object value = getValue(name);
if (value instanceof String) {
return (String) value;
} else {
return defaultValue;
}
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof DefaultFigureStyle)) {
return false;
}
DefaultFigureStyle other = (DefaultFigureStyle) obj;
Property[] properties = this.getProperties();
for (Property property : properties) {
Property otherProperty = other.getProperty(property.getName());
if (otherProperty == null || otherProperty.getValue() != null && property.getValue() == null ||
(property.getValue() != null && !property.getValue().equals(otherProperty.getValue()))) {
return false;
}
}
return true;
}
}