com.hfg.svg.AbstractSvgNode Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com_hfg Show documentation
Show all versions of com_hfg Show documentation
com.hfg xml, html, svg, and bioinformatics utility library
package com.hfg.svg;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hfg.css.*;
import com.hfg.graphics.Graphics2DState;
import com.hfg.graphics.GraphicsUtil;
import com.hfg.graphics.units.GfxSize;
import com.hfg.graphics.units.GfxUnits;
import com.hfg.html.attribute.HTMLColor;
import com.hfg.util.StringBuilderPlus;
import com.hfg.util.StringUtil;
import com.hfg.util.collection.CollectionUtil;
import com.hfg.util.io.GZIP;
import com.hfg.xml.*;
//------------------------------------------------------------------------------
/**
* SVG (Scalable Vector Graphics) tag base class.
*
* @author J. Alex Taylor, hairyfatguy.com
*/
//------------------------------------------------------------------------------
// com.hfg XML/HTML Coding Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------
public abstract class AbstractSvgNode extends XMLTag implements SvgNode
{
private static final Pattern FILL_PATTERN = Pattern.compile("\\bfill\\s*:\\s*([^;\\s]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern OPACITY_PATTERN = Pattern.compile("\\bopacity\\s*:\\s*([^;\\s]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern FILL_OPACITY_PATTERN = Pattern.compile("\\bfill-opacity\\s*:\\s*([^;\\s]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern STROKE_PATTERN = Pattern.compile("\\bstroke\\s*:\\s*([^;$]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern STROKE_WIDTH_PATTERN = Pattern.compile("\\bstroke-width\\s*:\\s*([^;\\s]+?)(:?px)?", Pattern.CASE_INSENSITIVE);
private static final Pattern STROKE_OPACITY_PATTERN = Pattern.compile("\\bstroke-opacity\\s*:\\s*([^;\\s]+)", Pattern.CASE_INSENSITIVE);
private static final Pattern TRANSFORM_PATTERN = Pattern.compile("\\btransform\\s*:\\s*([^;$]+)", Pattern.CASE_INSENSITIVE);
private XMLTag mTitleTag;
//---------------------------------------------------------------------------
public AbstractSvgNode(String inTagName)
{
super(inTagName);
setNamespace(XMLNamespace.SVG);
}
//---------------------------------------------------------------------------
protected void initFromXMLTag(XMLTag inXMLTag)
{
inXMLTag.verifyTagName(getTagName());
if (CollectionUtil.hasValues(inXMLTag.getAttributes()))
{
for (XMLAttribute attr : inXMLTag.getAttributes())
{
setAttribute(attr.clone());
}
}
List subtagsAndContent = inXMLTag.getContentPlusSubtagList();
if (CollectionUtil.hasValues(subtagsAndContent))
{
for (Object object : subtagsAndContent)
{
if (object instanceof XMLTag)
{
addSubtag(SVG.constructFromXMLTag((XMLTag) object));
}
else if (object instanceof XMLizable)
{
addSubtag((XMLizable) object);
}
else
{
String content;
if (object instanceof byte[])
{
// Large content gets gzip-compressed
content = GZIP.uncompressToString((byte[]) object);
}
else
{
content = (String) object;
}
int index = content.indexOf("= 0)
{
boolean hideWithCommentsFromLegacyBrowsers = false;
content = content.substring(index + 9);
if (content.startsWith("//> cssDeclarations = getCSSDeclarations(inCSS);
applyTransforms(g2);
Composite composite = getG2Composite();
if (composite != null)
{
g2.setComposite(composite);
}
Font locallyAdjustedFont = getAdjustedFont(origState.getFont(), cssDeclarations);
if (locallyAdjustedFont != null)
{
g2.setFont(locallyAdjustedFont);
}
drawSubnodes(g2, inCSS);
// Restore settings
origState.applyTo(g2);
}
//---------------------------------------------------------------------------
public String getCssTransform()
{
String transform = null;
String style = getAttributeValue(SvgAttr.style);
if (StringUtil.isSet(style))
{
List cssDeclarations = CSSDeclaration.parse(style);
for (CSSDeclaration cssDeclaration : cssDeclarations)
{
if (cssDeclaration.getProperty().equals(CSSProperty.transform))
{
transform = cssDeclaration.getValue();
break;
}
}
}
return transform;
}
//---------------------------------------------------------------------------
/**
Specifies the upper left corner of the bounding rectangle.
*/
public SvgNode setPosition(Point2D inValue)
{
Rectangle2D bbox = getBoundsBox();
StringBuilderPlus transform = new StringBuilderPlus(getCssTransform()).setDelimiter(" ");
transform.delimitedAppend("translate(" + (inValue.getX() - bbox.getX()) + ", " + (inValue.getY() - bbox.getY()) + ")");
setTransform(transform.toString());
return this;
}
//---------------------------------------------------------------------------
public Rectangle2D getBoundsBox()
{
Double minX = null;
String xString = getAttributeValue(SvgAttr.x);
if (StringUtil.isSet(xString))
{
if (xString.endsWith("%"))
{
// TODO: How to handle percents?
}
else
{
minX = Double.parseDouble(xString);
}
}
Double minY = null;
String yString = getAttributeValue(SvgAttr.y);
if (StringUtil.isSet(yString))
{
if (yString.endsWith("%"))
{
// TODO: How to handle percents?
}
else
{
minY = Double.parseDouble(yString);
}
}
Double maxX = null;
Double maxY = null;
for (XMLizable node : getSubtags())
{
if (node instanceof SvgNode)
{
Rectangle2D rect = ((SvgNode)node).getBoundsBox();
if (rect != null)
{
if (null == minX || rect.getX() < minX) minX = rect.getX();
if (null == minY || rect.getY() < minY) minY = rect.getY();
if (null == maxX || rect.getMaxX() > maxX) maxX = rect.getMaxX();
if (null == maxY || rect.getMaxY() > maxY) maxY = rect.getMaxY();
}
}
}
Rectangle2D boundsBox = null;
if (minX != null
&& minY != null
&& maxX != null
&& maxY != null)
{
boundsBox = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
adjustBoundsForTransform(boundsBox);
}
return boundsBox;
}
//---------------------------------------------------------------------------
public Point2D getCenterPoint()
{
Rectangle2D bbox = getBoundsBox();
return new Point2D.Double(bbox.getX() + (bbox.getWidth() / 2f),
bbox.getY() + (bbox.getHeight()) / 2f);
}
//---------------------------------------------------------------------------
protected void rangeCheckOpacityValue(float inValue)
{
if (inValue > 1 || inValue < 0)
{
throw new RuntimeException("Illegal opacity value: " + inValue + "! Must be between 0 and 1 (inclusive).");
}
}
//---------------------------------------------------------------------------
// TODO: Font style and weight
protected Font getAdjustedFont(Font inOrigFont, List inCSSDeclarations)
{
Font font = null;
if (CollectionUtil.hasValues(inCSSDeclarations))
{
for (CSSDeclaration cssDeclaration : inCSSDeclarations)
{
if (cssDeclaration.getProperty().equals(CSSProperty.font_size))
{
if (null == font)
{
font = new Font(inOrigFont.getFontName(), inOrigFont.getStyle(), inOrigFont.getSize());
}
// GfxSize size = GfxSize.allocate(cssDeclaration.getValue(), GfxUnits.pixels);
// font = new Font(font.getFontName(), font.getStyle(), size.toInt(GfxUnits.points));
int fontSize;
try
{
fontSize = Integer.parseInt(cssDeclaration.getValue());
}
catch (NumberFormatException e)
{
GfxSize size = GfxSize.allocate(cssDeclaration.getValue());
fontSize = size.toInt(GfxUnits.points);
}
font = new Font(font.getFontName(), font.getStyle(), fontSize);
}
else if (cssDeclaration.getProperty().equals(CSSProperty.font_family))
{
if (null == font)
{
font = new Font(inOrigFont.getFontName(), inOrigFont.getStyle(), inOrigFont.getSize());
}
font = new Font(cssDeclaration.getValue(), font.getStyle(), font.getSize());
}
}
}
if (hasAttribute(SvgAttr.fontSize))
{
if (null == font)
{
font = new Font(inOrigFont.getFontName(), inOrigFont.getStyle(), inOrigFont.getSize());
}
GfxSize size = GfxSize.allocate(getAttributeValue(SvgAttr.fontSize), GfxUnits.pixels);
font = new Font(font.getFontName(), font.getStyle(), size.toInt(GfxUnits.points));
}
if (hasAttribute(SvgAttr.fontFamily))
{
if (null == font)
{
font = new Font(inOrigFont.getFontName(), inOrigFont.getStyle(), inOrigFont.getSize());
}
font = new Font(getAttributeValue(SvgAttr.fontFamily), font.getStyle(), font.getSize());
}
return font;
}
//--------------------------------------------------------------------------
protected void applyTransforms(Graphics2D g2)
{
if (getAttributeValue(SvgAttr.transform) != null)
{
applyTransform(g2, getAttributeValue(SvgAttr.transform));
}
if (StringUtil.isSet(getAttributeValue(SvgAttr.style)))
{
Matcher m = TRANSFORM_PATTERN.matcher(getAttributeValue(SvgAttr.style));
if (m.find())
{
applyTransform(g2, m.group(1));
}
}
}
//---------------------------------------------------------------------------
protected void applyTransform(Graphics2D g2, String inTransformAttributeValue)
{
// Ex: "rotate(-90.0 118 9) translate(-33.0 3.0 )"
if (StringUtil.isSet(inTransformAttributeValue))
{
String transformValue = inTransformAttributeValue.trim();
int index;
while ((index = transformValue.indexOf("(")) > 0)
{
String functionName = transformValue.substring(0, index).trim();
int endArgIndex = transformValue.indexOf(")");
String[] functionArgs = transformValue.substring(index + 1, endArgIndex).split("\\s*[,\\s]\\s*");
for (int i = 0; i < functionArgs.length; i++)
{
if (functionArgs[i].endsWith("px"))
{
functionArgs[i] = functionArgs[i].substring(0, functionArgs[i].length() - 2);
}
else if (functionArgs[i].endsWith("deg"))
{
functionArgs[i] = functionArgs[i].substring(0, functionArgs[i].length() - 3);
}
}
switch (functionName)
{
case "rotate":
// The first arg is the rotation angle in degrees. The optional second and third args are the anchor point for rotation.
// If no anchor point is specified, used the center point of the bounds box.
Rectangle2D contentRectangle = getBoundsBox();
double anchorPointX = (3 == functionArgs.length ? Double.parseDouble(functionArgs[1]) : contentRectangle.getX() + (contentRectangle.getWidth() / 2));
double anchorPointY = (3 == functionArgs.length ? Double.parseDouble(functionArgs[2]) : contentRectangle.getY() + (contentRectangle.getHeight() / 2));
AffineTransform rotation = new AffineTransform();
rotation.rotate(Math.toRadians(Double.parseDouble(functionArgs[0])), anchorPointX, anchorPointY);
g2.transform(rotation);
break;
case "translate":
AffineTransform translation = new AffineTransform();
// The y value is optional. Use zero if it was not specified
double yTranslation = (functionArgs.length > 1 ? Double.parseDouble(functionArgs[1]) : 0);
translation.translate(Double.parseDouble(functionArgs[0]), yTranslation);
g2.transform(translation);
break;
case "translateX":
translation = new AffineTransform();
translation.translate(Double.parseDouble(functionArgs[0]), 0);
g2.transform(translation);
break;
case "translateY":
translation = new AffineTransform();
translation.translate(0, Double.parseDouble(functionArgs[0]));
g2.transform(translation);
break;
default:
System.err.println(StringUtil.singleQuote(functionName) + " is not a currently supported transform function for conversion to Graphics2D!");
}
if (endArgIndex < transformValue.length() - 1)
{
transformValue = transformValue.substring(endArgIndex + 1);
}
else
{
break;
}
}
}
}
//--------------------------------------------------------------------------
protected void adjustBoundsForTransform(Rectangle2D inBoundsBox)
{
if (hasAttribute(SvgAttr.transform))
{
String value = getAttributeValue(SvgAttr.transform);
Matcher m = SvgAttr.TRANSLATE_PATTERN.matcher(value);
if (m.find())
{
float translateX = Float.parseFloat(m.group(1));
float translateY = Float.parseFloat(m.group(2));
// inBoundsBox.translate(translateX, translateY);
inBoundsBox.setRect(inBoundsBox.getX() + translateX, inBoundsBox.getY() + translateY, inBoundsBox.getWidth(), inBoundsBox.getHeight());
}
else
{
m = SvgAttr.ROTATE_PATTERN.matcher(value);
if (m.find())
{
Point2D centerOfRotation;
float angle = Float.parseFloat(m.group(1));
if (m.group(2) != null)
{
// A center of rotation was specified
centerOfRotation = new Point2D.Float(Float.parseFloat(m.group(2)),
Float.parseFloat(m.group(3)));
}
else
{
// Default the center of rotation to the center of the bounds box
centerOfRotation = new Point2D.Double(inBoundsBox.getX() + (inBoundsBox.getWidth() / 2),
inBoundsBox.getY() + (inBoundsBox.getHeight() / 2));
}
Rectangle2D adjustedBounds = GraphicsUtil.rotate(inBoundsBox, angle, centerOfRotation).getBounds2D();
inBoundsBox.setRect(adjustedBounds.getX(), adjustedBounds.getY(), adjustedBounds.getWidth(), adjustedBounds.getHeight());
}
}
}
}
//--------------------------------------------------------------------------
protected void drawSubnodes(Graphics2D g2, CSS inCSS)
{
if (CollectionUtil.hasValues(getSubtags()))
{
for (XMLizable node : getSubtags())
{
if (node instanceof AbstractSvgNode)
{
((AbstractSvgNode)node).draw(g2, inCSS);
}
}
}
}
//--------------------------------------------------------------------------
protected Composite getG2Composite()
{
String opacityString = null;
if (StringUtil.isSet(getAttributeValue(SvgAttr.opacity)))
{
if (! getAttributeValue(SvgAttr.opacity).equals(SvgAttr.Value.none))
{
opacityString = getAttributeValue(SvgAttr.opacity);
}
}
else if (StringUtil.isSet(getAttributeValue(SvgAttr.style)))
{
Matcher m = OPACITY_PATTERN.matcher(getAttributeValue(SvgAttr.style));
if (m.find())
{
opacityString = m.group(1);
}
}
Composite composite = null;
if (StringUtil.isSet(opacityString))
{
composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, Float.parseFloat(opacityString.trim()));
}
return composite;
}
//--------------------------------------------------------------------------
protected Paint getG2Paint(List inCSSDeclarations)
{
String colorString = null;
if (StringUtil.isSet(getAttributeValue(SvgAttr.fill)))
{
if (! getAttributeValue(SvgAttr.fill).equals(SvgAttr.Value.none))
{
colorString = getAttributeValue(SvgAttr.fill);
}
}
else if (CollectionUtil.hasValues(inCSSDeclarations))
{
for (CSSDeclaration cssDeclaration : inCSSDeclarations)
{
if (cssDeclaration.getProperty().name().equals(SvgAttr.fill))
{
colorString = cssDeclaration.getValue();
}
}
}
Color color = null;
if (StringUtil.isSet(colorString))
{
if (colorString.startsWith("#"))
{
color = Color.decode(colorString);
}
else
{
color = Color.getColor(colorString);
}
}
String opacityString = null;
if (StringUtil.isSet(getAttributeValue(SvgAttr.fillOpacity)))
{
if (! getAttributeValue(SvgAttr.fillOpacity).equals(SvgAttr.Value.none))
{
opacityString = getAttributeValue(SvgAttr.fillOpacity);
}
}
else if (CollectionUtil.hasValues(inCSSDeclarations))
{
for (CSSDeclaration cssDeclaration : inCSSDeclarations)
{
if (cssDeclaration.getProperty().name().equals(SvgAttr.fillOpacity))
{
opacityString = cssDeclaration.getValue();
}
}
}
if (color != null
&& StringUtil.isSet(opacityString))
{
color = new Color(color.getRed()/255f, color.getGreen()/255f, color.getBlue()/255f, Float.parseFloat(opacityString));
}
return color;
}
//--------------------------------------------------------------------------
protected BasicStroke getG2Stroke(List inCSSDeclarations)
{
BasicStroke stroke = null;
if ("none".equalsIgnoreCase(getAttributeValue(SvgAttr.stroke)))
{
// We were requested not to display the stroke so give it zero width
stroke = new BasicStroke(0f);
}
else if (StringUtil.isSet(getAttributeValue(SvgAttr.strokeWidth)))
{
stroke = new BasicStroke(Float.parseFloat(getAttributeValue(SvgAttr.strokeWidth)));
}
else if (CollectionUtil.hasValues(inCSSDeclarations))
{
for (CSSDeclaration cssDeclaration : inCSSDeclarations)
{
if (cssDeclaration.getProperty().name().equals(SvgAttr.strokeWidth))
{
try
{
stroke = new BasicStroke(Float.parseFloat(cssDeclaration.getValue()));
}
catch (NumberFormatException e)
{
GfxSize size = GfxSize.allocate(cssDeclaration.getValue());
stroke = new BasicStroke(size.to(GfxUnits.pixels));
}
}
}
}
return stroke;
}
//--------------------------------------------------------------------------
protected Color getG2StrokeColor(List inCSSDeclarations)
{
String colorString = null;
if (StringUtil.isSet(getAttributeValue(SvgAttr.stroke)))
{
colorString = getAttributeValue(SvgAttr.stroke);
}
else if (CollectionUtil.hasValues(inCSSDeclarations))
{
for (CSSDeclaration cssDeclaration : inCSSDeclarations)
{
if (cssDeclaration.getProperty().name().equals(SvgAttr.stroke))
{
colorString = cssDeclaration.getValue();
}
}
}
Color color = null;
if (StringUtil.isSet(colorString))
{
if (colorString.startsWith("#"))
{
try
{
color = Color.decode(colorString);
}
catch (Exception e)
{
throw new RuntimeException("Problem decoding color string " + StringUtil.singleQuote(colorString) + "!");
}
}
else
{
color = HTMLColor.valueOf(colorString);
}
}
String opacityString = null;
if (StringUtil.isSet(getAttributeValue(SvgAttr.strokeOpacity)))
{
if (! getAttributeValue(SvgAttr.strokeOpacity).equals(SvgAttr.Value.none))
{
opacityString = getAttributeValue(SvgAttr.strokeOpacity);
}
}
else if (StringUtil.isSet(getAttributeValue(SvgAttr.style)))
{
Matcher m = STROKE_OPACITY_PATTERN.matcher(getAttributeValue(SvgAttr.style));
if (m.find())
{
opacityString = m.group(1);
}
}
if (StringUtil.isSet(opacityString))
{
if (null == color)
{
color = Color.BLACK;
}
float opacityValue;
if (opacityString.endsWith("%"))
{
opacityValue = Float.parseFloat(opacityString.substring(0, opacityString.length() - 1)) / 100f;
}
else
{
opacityValue = Float.parseFloat(opacityString);
}
color = new Color(color.getRed()/255f, color.getGreen()/255f, color.getBlue()/255f, opacityValue);
}
return color;
}
//---------------------------------------------------------------------------
// Returns the in-line and CSS-defined styling for the specified tag.
protected List getCSSDeclarations(CSS inCSS)
{
List cssDeclarations = new ArrayList<>(20);
if (inCSS != null)
{
List declarations = inCSS.getCSSDeclarationsForSvgNode(this, CSSMediaType.print);
if (CollectionUtil.hasValues(declarations))
{
cssDeclarations.addAll(declarations);
}
}
String styleString = getAttributeValue(SvgAttr.style);
if (StringUtil.isSet(styleString))
{
List styleCSSDeclarations = CSSDeclaration.parse(styleString);
if (CollectionUtil.hasValues(styleCSSDeclarations))
{
cssDeclarations.addAll(styleCSSDeclarations);
}
}
return cssDeclarations;
}
//---------------------------------------------------------------------------
// Walks up the parent chain until it finds the SVG object.
protected SVG findParentSVG()
{
SVG parentSVG = null;
SvgNode svgNode = this;
while (svgNode != null)
{
if (svgNode.getTagName().equals(SVG.svg))
{
parentSVG = (SVG) svgNode;
break;
}
svgNode = (SvgNode) svgNode.getParentNode();
}
return parentSVG;
}
}