package com.revolsys.swing.map.layer.record.style; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.font.FontRenderContext; import java.awt.font.TextLayout; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; import javax.measure.Measure; import javax.measure.quantity.Length; import javax.measure.unit.Unit; import com.revolsys.awt.WebColors; import com.revolsys.collection.map.LinkedHashMapEx; import com.revolsys.collection.map.MapEx; import com.revolsys.datatype.DataType; import com.revolsys.io.map.MapSerializer; import com.revolsys.logging.Logs; import com.revolsys.properties.BaseObjectWithPropertiesAndChange; import com.revolsys.swing.map.Viewport2D; import com.revolsys.swing.map.layer.record.renderer.TextStyleRenderer; import com.revolsys.util.Property; import com.revolsys.util.Strings; public class TextStyle extends BaseObjectWithPropertiesAndChange implements MapSerializer, Cloneable { private static final String AUTO = "auto"; private static final Map<String, Object> DEFAULT_VALUES = new TreeMap<>(); private static final Set<String> PROPERTY_NAMES = new HashSet<>(); static { // addProperty("text-allow-overlap",DataTypes.); // addProperty("text-avoid-edges",DataTypes.); addStyleProperty("textBoxColor", WebColors.Gainsboro); addStyleProperty("textBoxOpacity", 255); // addProperty("text-character-spacing",DataTypes.); // addProperty("text-clip",DataTypes.); // addProperty("text-comp-op",DataTypes.); addStyleProperty("textDx", MarkerStyle.ZERO_PIXEL); addStyleProperty("textDy", MarkerStyle.ZERO_PIXEL); addStyleProperty("textFaceName", "Arial"); addStyleProperty("textFill", WebColors.Black); addStyleProperty("textHaloFill", WebColors.White); addStyleProperty("textHaloRadius", 0); addStyleProperty("textHorizontalAlignment", AUTO); // addProperty("text-label-position-tolerance",DataTypes.); // addProperty("text-line-spacing",DataTypes.); // addProperty("text-max-char-angle-delta",DataTypes.); // addProperty("text-min-distance",DataTypes.); // addProperty("text-min-padding",DataTypes.); // addProperty("text-min-path-length",DataTypes.); addStyleProperty("textName", ""); addStyleProperty("textOpacity", 255); addStyleProperty("textOrientation", 0.0); addStyleProperty("textOrientationType", AUTO); // addProperty("text-placement",DataTypes.); addStyleProperty("textPlacementType", AUTO); // addProperty("text-placements",DataTypes.); // addProperty("text-ratio",DataTypes.); addStyleProperty("textSize", MarkerStyle.TEN_PIXELS); // addProperty("text-spacing",DataTypes.); // addProperty("text-transform",DataTypes.); addStyleProperty("textVerticalAlignment", AUTO); // addProperty("text-wrap-before",DataTypes.); // addProperty("text-wrap-character",DataTypes.); // addProperty("text-wrap-width", Double.class); } private static final void addStyleProperty(final String name, final Object defaultValue) { PROPERTY_NAMES.add(name); DEFAULT_VALUES.put(name, defaultValue); } public static TextStyle text() { return new TextStyle(); } private Font font; private long lastScale = 0; private Color textBoxColor = WebColors.Gainsboro; private int textBoxOpacity = 255; private Measure<Length> textDx = GeometryStyle.ZERO_PIXEL; private Measure<Length> textDy = GeometryStyle.ZERO_PIXEL; private String textFaceName = "Arial"; private Color textFill = WebColors.Black; private Color textHaloFill = WebColors.White; private double textHaloRadius = 0; private String textHorizontalAlignment = AUTO; private String textName = ""; private int textOpacity = 255; /** The orientation of the text in a clockwise direction from the east axis. */ private double textOrientation = 0; private String textOrientationType = AUTO; private String textPlacementType = AUTO; private Measure<Length> textSize = GeometryStyle.TEN_PIXELS; private String textVerticalAlignment = AUTO; public TextStyle() { } public TextStyle(final Map<String, Object> style) { setProperties(style); } @Override public TextStyle clone() { return (TextStyle)super.clone(); } public void drawTextIcon(final Graphics2D graphics, final int size) { double orientation = getTextOrientation(); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); final String textFaceName = getTextFaceName(); final Font font = new Font(textFaceName, 0, size); graphics.setFont(font); final FontMetrics fontMetrics = graphics.getFontMetrics(); int min; int max; int boxWidth; if (size == 12) { min = 0; max = 15; boxWidth = 16; } else { min = 2; max = 12; boxWidth = 12; } final String text = "A"; final Rectangle2D bounds = fontMetrics.getStringBounds(text, graphics); final double width = bounds.getWidth(); final double height = fontMetrics.getAscent(); final String horizontalAlignment = getTextHorizontalAlignment(); final int x; if ("right".equals(horizontalAlignment)) { x = max - (int)Math.round(width); } else if ("center".equals(horizontalAlignment) || "auto".equals(horizontalAlignment)) { x = 8 - (int)Math.round(width / 2); } else { x = min; } final String verticalAlignment = getTextVerticalAlignment(); final int y; if ("top".equals(verticalAlignment)) { y = (int)height + min - 1; } else if ("middle".equals(verticalAlignment) || "auto".equals(verticalAlignment)) { y = 7 + (int)Math.round(height / 2); } else { y = 16 - min; } if (orientation != 0) { if (orientation > 270) { orientation -= 360; } graphics.rotate(-Math.toRadians(orientation), 8, 8); } final int textBoxOpacity = getTextBoxOpacity(); final Color textBoxColor = getTextBoxColor(); if (textBoxOpacity > 0 && textBoxColor != null) { graphics.setPaint(textBoxColor); final RoundRectangle2D.Double box = new RoundRectangle2D.Double(min, min, boxWidth, boxWidth, 5, 5); graphics.fill(box); } final double textHaloRadius = getTextHaloRadius(); if (textHaloRadius > 0) { final Stroke savedStroke = graphics.getStroke(); final Stroke outlineStroke = new BasicStroke((float)(textHaloRadius + 1), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); graphics.setColor(getTextHaloFill()); graphics.setStroke(outlineStroke); final FontRenderContext fontRenderContext = graphics.getFontRenderContext(); final TextLayout textLayout = new TextLayout(text, font, fontRenderContext); final Shape outlineShape = textLayout.getOutline(TextStyleRenderer.NOOP_TRANSFORM); graphics.draw(outlineShape); graphics.setStroke(savedStroke); } graphics.setColor(getTextFill()); if (textBoxOpacity > 0 && textBoxOpacity < 255) { graphics.setComposite(AlphaComposite.SrcOut); graphics.drawString(text, x, y); graphics.setComposite(AlphaComposite.DstOver); graphics.drawString(text, x, y); } else { graphics.setComposite(AlphaComposite.SrcOver); graphics.drawString(text, x, y); } } public Font getFont(final Viewport2D viewport) { final int style = 0; // if (textStyle.getFontWeight() == FontWeight.BOLD) { // style += Font.BOLD; // } // if (textStyle.getFontStyle() == FontStyle.ITALIC) { // style += Font.ITALIC; // } final double fontSize = Viewport2D.toDisplayValue(viewport, this.textSize); return new Font(this.textFaceName, style, (int)Math.ceil(fontSize)); } public Color getTextBoxColor() { return this.textBoxColor; } public int getTextBoxOpacity() { return this.textBoxOpacity; } public Measure<Length> getTextDx() { return this.textDx; } public Measure<Length> getTextDy() { return this.textDy; } public String getTextFaceName() { return this.textFaceName; } public Color getTextFill() { return this.textFill; } public Color getTextHaloFill() { return this.textHaloFill; } public double getTextHaloRadius() { return this.textHaloRadius; } public String getTextHorizontalAlignment() { return this.textHorizontalAlignment; } public String getTextName() { return this.textName; } public int getTextOpacity() { return this.textOpacity; } public double getTextOrientation() { return this.textOrientation; } public String getTextOrientationType() { return this.textOrientationType; } public String getTextPlacementType() { return this.textPlacementType; } public Measure<Length> getTextSize() { return this.textSize; } public Unit<Length> getTextSizeUnit() { return this.textSize.getUnit(); } public String getTextVerticalAlignment() { return this.textVerticalAlignment; } @Override public void setPropertyError(final String name, final Object value, final Throwable e) { Logs.error(this, "Error setting " + name + '=' + value, e); } public void setTextBoxColor(final Color textBoxColor) { final Object oldTextBoxColor = this.textBoxColor; final Object oldTextBoxOpacity = this.textBoxOpacity; if (textBoxColor == null) { this.textBoxColor = null; this.textBoxOpacity = 255; } else { this.textBoxColor = textBoxColor; this.textBoxOpacity = textBoxColor.getAlpha(); } firePropertyChange("textBoxColor", oldTextBoxColor, this.textBoxColor); firePropertyChange("textBoxOpacity", oldTextBoxOpacity, this.textBoxOpacity); } public void setTextBoxOpacity(final int textBoxOpacity) { final Object oldTextBoxColor = this.textBoxColor; final Object oldTextBoxOpacity = this.textBoxOpacity; if (textBoxOpacity < 0 || textBoxOpacity > 255) { throw new IllegalArgumentException("Text box opacity must be between 0 - 255"); } else { this.textBoxOpacity = textBoxOpacity; this.textBoxColor = WebColors.newAlpha(this.textBoxColor, this.textBoxOpacity); } firePropertyChange("textBoxColor", oldTextBoxColor, this.textBoxColor); firePropertyChange("textBoxOpacity", oldTextBoxOpacity, this.textBoxOpacity); } public void setTextDx(final Measure<Length> textDx) { final Object oldValue = this.textDy; if (textDx == null) { this.textDx = this.textDy; } else { this.textDx = textDx; } firePropertyChange("textDx", oldValue, this.textDx); updateTextDeltaUnits(this.textDx.getUnit()); } public void setTextDy(final Measure<Length> textDy) { final Object oldValue = this.textDy; if (textDy == null) { this.textDy = this.textDx; } else { this.textDy = textDy; } firePropertyChange("textDy", oldValue, this.textDy); updateTextDeltaUnits(this.textDy.getUnit()); } public void setTextFaceName(final String textFaceName) { final Object oldValue = this.textFaceName; this.textFaceName = textFaceName; this.font = null; firePropertyChange("textFaceName", oldValue, this.textFaceName); } public void setTextFill(final Color fill) { final Object oldTextFill = this.textFill; final Object oldTextOpacity = this.textOpacity; if (fill == null) { this.textFill = new Color(0, 0, 0, this.textOpacity); } else { this.textFill = fill; this.textOpacity = fill.getAlpha(); } firePropertyChange("textFill", oldTextFill, this.textFill); firePropertyChange("textOpacity", oldTextOpacity, this.textOpacity); } public void setTextHaloFill(final Color fill) { final Object oldValue = this.textHaloFill; if (fill == null) { this.textHaloFill = new Color(0, 0, 0, this.textOpacity); } else { this.textHaloFill = WebColors.newAlpha(fill, this.textOpacity); } firePropertyChange("textHaloFill", oldValue, this.textHaloFill); } public void setTextHaloRadius(final double textHaloRadius) { final Object oldValue = this.textHaloRadius; this.textHaloRadius = textHaloRadius; firePropertyChange("textHaloRadius", oldValue, this.textHaloRadius); } public void setTextHaloRadius(final Measure<Length> textHaloRadius) { setTextHaloRadius(textHaloRadius.doubleValue(textHaloRadius.getUnit())); } public void setTextHorizontalAlignment(final String textHorizontalAlignment) { final Object oldValue = this.textHorizontalAlignment; if (Property.hasValue(textHorizontalAlignment)) { this.textHorizontalAlignment = textHorizontalAlignment; } else { this.textHorizontalAlignment = AUTO; } firePropertyChange("textHorizontalAlignment", oldValue, this.textHorizontalAlignment); } public void setTextName(final String textName) { final Object oldValue = this.textName; if (textName == null) { this.textName = ""; } else { this.textName = textName; } firePropertyChange("textName", oldValue, this.textName); } public void setTextOpacity(final int textOpacity) { final Object oldTextFill = this.textFill; final Object oldTextOpacity = this.textOpacity; final Object oldTextHaloFill = this.textHaloFill; if (textOpacity < 0 || textOpacity > 255) { throw new IllegalArgumentException("Text opacity must be between 0 - 255"); } else { this.textOpacity = textOpacity; this.textFill = WebColors.newAlpha(this.textFill, this.textOpacity); this.textHaloFill = WebColors.newAlpha(this.textHaloFill, this.textOpacity); } firePropertyChange("textFill", oldTextFill, this.textFill); firePropertyChange("textOpacity", oldTextOpacity, this.textOpacity); firePropertyChange("textHaloFill", oldTextHaloFill, this.textHaloFill); } public void setTextOrientation(final double textOrientation) { final Object oldValue = this.textOrientation; this.textOrientation = textOrientation; firePropertyChange("textOrientation", oldValue, this.textOrientation); } public void setTextOrientationType(final String textOrientationType) { final Object oldValue = this.textOrientationType; this.textOrientationType = textOrientationType; firePropertyChange("textOrientationType", oldValue, this.textOrientationType); } @Deprecated public void setTextPlacement(final String textPlacementType) { setTextPlacementType(textPlacementType); } public void setTextPlacementType(String textPlacementType) { final Object oldValue = this.textPlacementType; if (Property.hasValue(textPlacementType)) { textPlacementType = Strings.replaceAll(textPlacementType, "^point\\(", "vertex\\("); this.textPlacementType = textPlacementType; } else { this.textPlacementType = AUTO; } firePropertyChange("textPlacementType", oldValue, this.textPlacementType); } public void setTextSize(final Measure<Length> textSize) { final Object oldValue = this.textSize; this.textSize = MarkerStyle.getWithDefault(textSize, MarkerStyle.TEN_PIXELS); this.font = null; firePropertyChange("textSize", oldValue, this.textSize); } public synchronized void setTextStyle(final Viewport2D viewport, final Graphics2D graphics) { if (viewport == null) { final Font font = new Font(this.textFaceName, 0, this.textSize.getValue().intValue()); graphics.setFont(font); } else { final long scale = (long)viewport.getScale(); if (this.font == null || this.lastScale != scale) { this.lastScale = scale; final int style = 0; // if (textStyle.getFontWeight() == FontWeight.BOLD) { // style += Font.BOLD; // } // if (textStyle.getFontStyle() == FontStyle.ITALIC) { // style += Font.ITALIC; // } final double fontSize = Viewport2D.toDisplayValue(viewport, this.textSize); this.font = new Font(this.textFaceName, style, (int)Math.ceil(fontSize)); } graphics.setFont(this.font); } } public void setTextVerticalAlignment(final String textVerticalAlignment) { final Object oldValue = this.textVerticalAlignment; if (Property.hasValue(textVerticalAlignment)) { this.textVerticalAlignment = textVerticalAlignment; } else { this.textVerticalAlignment = AUTO; } firePropertyChange("textVerticalAlignment", oldValue, this.textVerticalAlignment); } @Override public MapEx toMap() { final MapEx map = new LinkedHashMapEx(); for (final String name : PROPERTY_NAMES) { Object value = Property.get(this, name); if (value instanceof Color) { final Color color = (Color)value; value = WebColors.newAlpha(color, 255); } boolean defaultEqual = false; if (DEFAULT_VALUES.containsKey(name)) { final Object defaultValue = DEFAULT_VALUES.get(name); defaultEqual = DataType.equal(defaultValue, value); } if (!defaultEqual) { addToMap(map, name, value); } } return map; } @Override public String toString() { return toMap().toString(); } private void updateTextDeltaUnits(final Unit<Length> unit) { if (!this.textDx.getUnit().equals(unit)) { final double oldValue = this.textDx.getValue().doubleValue(); final Measure<Length> newValue = Measure.valueOf(oldValue, unit); setTextDx(newValue); } if (!this.textDy.getUnit().equals(unit)) { final double oldValue = this.textDy.getValue().doubleValue(); final Measure<Length> newValue = Measure.valueOf(oldValue, unit); setTextDy(newValue); } } }