com.kitfox.svg.SVGElement Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of svg-salamander Show documentation
Show all versions of svg-salamander Show documentation
SVG Salamander - tools and components for SVG rendering, manipulation and animation
The newest version!
/*
* SVGElement.java
*
*
* The Salamander Project - 2D and 3D graphics libraries in Java
* Copyright (C) 2004 Mark McKay
*
* 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
*
* Mark McKay can be contacted at [email protected]. Salamander and other
* projects can be found at http://www.kitfox.com
*
* Created on January 26, 2004, 1:59 AM
*/
package com.kitfox.svg;
import java.util.*;
import java.util.regex.*;
import java.net.*;
import java.awt.geom.*;
import org.xml.sax.*;
import com.kitfox.svg.animation.*;
import com.kitfox.svg.pathcmd.*;
import com.kitfox.svg.xml.*;
import java.io.Serializable;
/**
* @author Mark McKay
* @author Mark McKay
*/
abstract public class SVGElement implements Serializable
{
public static final long serialVersionUID = 0;
public static final String SVG_NS = "http://www.w3.org/2000/svg";
protected SVGElement parent = null;
protected final ArrayList children = new ArrayList();
protected String id = null;
/**
* CSS class. Used for applying style sheet information.
*/
protected String cssClass = null;
/**
* Styles defined for this elemnt via the style attribute.
*/
protected final HashMap inlineStyles = new HashMap();
/**
* Presentation attributes set for this element. Ie, any attribute other
* than the style attribute.
*/
protected final HashMap presAttribs = new HashMap();
/**
* A list of presentation attributes to not include in the presentation
* attribute set.
*/
protected static final Set ignorePresAttrib;
static
{
HashSet set = new HashSet();
// set.add("id");
// set.add("class");
// set.add("style");
// set.add("xml:base");
ignorePresAttrib = Collections.unmodifiableSet(set);
}
/**
* This element may override the URI we resolve against with an
* xml:base attribute. If so, a copy is placed here. Otherwise, we defer
* to our parent for the reolution base
*/
protected URI xmlBase = null;
/**
* The diagram this element belongs to
*/
protected SVGDiagram diagram;
/**
* Link to the universe we reside in
*/
// protected SVGUniverse universe;
protected final TrackManager trackManager = new TrackManager();
boolean dirty = true;
// public static final Matcher adobeId = Pattern.compile("(.*)_1_").matcher("");
// static final String fpNumRe = "[-+]?((\\d+)|(\\d*\\.\\d+))([-+]?[eE]\\d+)?";
/** Creates a new instance of SVGElement */
public SVGElement()
{
this(null, null, null);
}
public SVGElement(String id, SVGElement parent)
{
this(id, null, parent);
}
public SVGElement(String id, String cssClass, SVGElement parent)
{
this.id = id;
this.cssClass = cssClass;
this.parent = parent;
}
public SVGElement getParent()
{
return parent;
}
void setParent(SVGElement parent)
{
this.parent = parent;
}
/**
* @return an ordered list of nodes from the root of the tree to this node
*/
public List getPath(List retVec)
{
if (retVec == null) retVec = new ArrayList();
if (parent != null)
{
parent.getPath(retVec);
}
retVec.add(this);
return retVec;
}
/**
* @param retVec - A list to add all children to. If null, a new list is
* created and children of this group are added.
*
* @return The list containing the children of this group
*/
public List getChildren(List retVec)
{
if (retVec == null) retVec = new ArrayList();
retVec.addAll(children);
return retVec;
}
/**
* @param id - Id of svg element to return
* @return the child of the given id, or null if no such child exists.
*/
public SVGElement getChild(String id)
{
for (Iterator it = children.iterator(); it.hasNext();)
{
SVGElement ele = (SVGElement)it.next();
String eleId = ele.getId();
if (eleId != null && eleId.equals(id)) return ele;
}
return null;
}
/**
* Searches children for given element. If found, returns index of child.
* Otherwise returns -1.
*/
public int indexOfChild(SVGElement child)
{
return children.indexOf(child);
}
/**
* Swaps 2 elements in children.
* @i index of first
* @j index of second
*
* @return true if successful, false otherwise
*/
public void swapChildren(int i, int j) throws SVGException
{
if ((children == null) || (i < 0) || (i >= children.size()) || (j < 0) || (j >= children.size()))
{
return;
}
Object temp = children.get(i);
children.set(i, children.get(j));
children.set(j, temp);
build();
}
/**
* Called during SAX load process to notify that this tag has begun the process
* of being loaded
* @param attrs - Attributes of this tag
* @param helper - An object passed to all SVG elements involved in this build
* process to aid in sharing information.
*/
public void loaderStartElement(SVGLoaderHelper helper, Attributes attrs, SVGElement parent) throws SAXException
{
//Set identification info
this.parent = parent;
this.diagram = helper.diagram;
this.id = attrs.getValue("id");
if (this.id != null && !this.id.equals(""))
{
diagram.setElement(this.id, this);
}
String className = attrs.getValue("class");
this.cssClass = (className == null || className.equals("")) ? null : className;
//docRoot = helper.docRoot;
//universe = helper.universe;
//Parse style string, if any
String style = attrs.getValue("style");
if (style != null)
{
HashMap map = XMLParseUtil.parseStyle(style, inlineStyles);
}
String base = attrs.getValue("xml:base");
if (base != null && !base.equals(""))
{
try
{
xmlBase = new URI(base);
}
catch (Exception e)
{
throw new SAXException(e);
}
}
//Place all other attributes into the presentation attribute list
int numAttrs = attrs.getLength();
for (int i = 0; i < numAttrs; i++)
{
String name = attrs.getQName(i);
if (ignorePresAttrib.contains(name)) continue;
String value = attrs.getValue(i);
presAttribs.put(name, new StyleAttribute(name, value));
}
}
public void addAttribute(String name, int attribType, String value) throws SVGElementException
{
if (hasAttribute(name, attribType)) throw new SVGElementException(this, "Attribute " + name + "(" + AnimationElement.animationElementToString(attribType) + ") already exists");
//Alter layout for id attribute
if ("id".equals(name) && diagram != null)
{
diagram.removeElement(this.id);
this.id = name;
diagram.setElement(this.id, this);
}
switch (attribType)
{
case AnimationElement.AT_CSS:
inlineStyles.put(name, new StyleAttribute(name, value));
return;
case AnimationElement.AT_XML:
presAttribs.put(name, new StyleAttribute(name, value));
return;
}
throw new SVGElementException(this, "Invalid attribute type " + attribType);
}
public boolean hasAttribute(String name, int attribType) throws SVGElementException
{
switch (attribType)
{
case AnimationElement.AT_CSS:
return inlineStyles.containsKey(name);
case AnimationElement.AT_XML:
return presAttribs.containsKey(name);
case AnimationElement.AT_AUTO:
return inlineStyles.containsKey(name) || presAttribs.containsKey(name);
}
throw new SVGElementException(this, "Invalid attribute type " + attribType);
}
/**
* @return a set of Strings that corespond to CSS attributes on this element
*/
public Set getInlineAttributes()
{
return inlineStyles.keySet();
}
/**
* @return a set of Strings that corespond to XML attributes on this element
*/
public Set getPresentationAttributes()
{
return presAttribs.keySet();
}
/**
* Called after the start element but before the end element to indicate
* each child tag that has been processed
*/
public void loaderAddChild(SVGLoaderHelper helper, SVGElement child) throws SVGElementException
{
children.add(child);
child.parent = this;
child.setDiagram(diagram);
//Add info to track if we've scanned animation element
if (child instanceof AnimationElement)
{
trackManager.addTrackElement((AnimationElement)child);
}
}
private void setDiagram(SVGDiagram diagram)
{
this.diagram = diagram;
diagram.setElement(id, this);
for (Iterator it = children.iterator(); it.hasNext();)
{
SVGElement ele = (SVGElement)it.next();
ele.setDiagram(diagram);
}
}
public void removeChild(SVGElement child) throws SVGElementException
{
if (!children.contains(child))
{
throw new SVGElementException(this, "Element does not contain child " + child);
}
children.remove(child);
}
/**
* Called during load process to add text scanned within a tag
*/
public void loaderAddText(SVGLoaderHelper helper, String text)
{
}
/**
* Called to indicate that this tag and the tags it contains have been completely
* processed, and that it should finish any load processes.
*/
public void loaderEndElement(SVGLoaderHelper helper) throws SVGParseException
{
try
{
build();
}
catch (SVGException se)
{
throw new SVGParseException(se);
}
}
/**
* Called by internal processes to rebuild the geometry of this node
* from it's presentation attributes, style attributes and animated tracks.
*/
protected void build() throws SVGException
{
StyleAttribute sty = new StyleAttribute();
if (getPres(sty.setName("id")))
{
String newId = sty.getStringValue();
if (!newId.equals(id))
{
diagram.removeElement(id);
id = newId;
diagram.setElement(this.id, this);
}
}
if (getPres(sty.setName("class"))) cssClass = sty.getStringValue();
if (getPres(sty.setName("xml:base"))) xmlBase = sty.getURIValue();
}
public URI getXMLBase()
{
return xmlBase != null ? xmlBase :
(parent != null ? parent.getXMLBase() : diagram.getXMLBase());
}
/**
* @return the id assigned to this node. Null if no id explicitly set.
*/
public String getId()
{
return id;
}
LinkedList contexts = new LinkedList();
/**
* Hack to allow nodes to temporarily change their parents. The Use tag will
* need this so it can alter the attributes that a particular node uses.
*/
protected void pushParentContext(SVGElement context)
{
contexts.addLast(context);
}
protected SVGElement popParentContext()
{
return (SVGElement)contexts.removeLast();
}
protected SVGElement getParentContext()
{
return contexts.isEmpty() ? null : (SVGElement)contexts.getLast();
}
/*
* Returns the named style attribute. Checks for inline styles first, then
* internal and extranal style sheets, and finally checks for presentation
* attributes.
* @param styleName - Name of attribute to return
* @param recursive - If true and this object does not contain the
* named style attribute, checks attributes of parents abck to root until
* one found.
*/
public boolean getStyle(StyleAttribute attrib) throws SVGException
{
return getStyle(attrib, true);
}
public void setAttribute(String name, int attribType, String value) throws SVGElementException
{
StyleAttribute styAttr;
switch (attribType)
{
case AnimationElement.AT_CSS:
{
styAttr = (StyleAttribute)inlineStyles.get(name);
break;
}
case AnimationElement.AT_XML:
{
styAttr = (StyleAttribute)presAttribs.get(name);
break;
}
case AnimationElement.AT_AUTO:
{
styAttr = (StyleAttribute)inlineStyles.get(name);
if (styAttr == null)
{
styAttr = (StyleAttribute)presAttribs.get(name);
}
break;
}
default:
throw new SVGElementException(this, "Invalid attribute type " + attribType);
}
if (styAttr == null)
{
throw new SVGElementException(this, "Could not find attribute " + name + "(" + AnimationElement.animationElementToString(attribType) + "). Make sure to create attribute before setting it.");
}
//Alter layout for relevant attributes
if ("id".equals(styAttr.getName()))
{
diagram.removeElement(this.id);
this.id = name;
diagram.setElement(this.id, this);
}
styAttr.setStringValue(value);
}
/**
* Copies the current style into the passed style attribute. Checks for
* inline styles first, then internal and extranal style sheets, and
* finally checks for presentation attributes. Recursively checks parents.
* @param attrib - Attribute to write style data to. Must have it's name
* set to the name of the style being queried.
* @param recursive - If true and this object does not contain the
* named style attribute, checks attributes of parents abck to root until
* one found.
*/
public boolean getStyle(StyleAttribute attrib, boolean recursive) throws SVGException
{
String styName = attrib.getName();
//Check for local inline styles
StyleAttribute styAttr = (StyleAttribute)inlineStyles.get(styName);
attrib.setStringValue(styAttr == null ? "" : styAttr.getStringValue());
//Evalutate coresponding track, if one exists
TrackBase track = trackManager.getTrack(styName, AnimationElement.AT_CSS);
if (track != null)
{
track.getValue(attrib, diagram.getUniverse().getCurTime());
return true;
}
//Return if we've found a non animated style
if (styAttr != null) return true;
//Implement style sheet lookup later
//Check for presentation attribute
StyleAttribute presAttr = (StyleAttribute)presAttribs.get(styName);
attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
//Evalutate coresponding track, if one exists
track = trackManager.getTrack(styName, AnimationElement.AT_XML);
if (track != null)
{
track.getValue(attrib, diagram.getUniverse().getCurTime());
return true;
}
//Return if we've found a presentation attribute instead
if (presAttr != null) return true;
//If we're recursive, check parents
if (recursive)
{
SVGElement parentContext = getParentContext();
if (parentContext != null) return parentContext.getStyle(attrib, true);
if (parent != null) return parent.getStyle(attrib, true);
}
//Unsuccessful reading style attribute
return false;
}
/**
* @return the raw style value of this attribute. Does not take the
* presentation value or animation into consideration. Used by animations
* to determine the base to animate from.
*/
public StyleAttribute getStyleAbsolute(String styName)
{
//Check for local inline styles
return (StyleAttribute)inlineStyles.get(styName);
}
/**
* Copies the presentation attribute into the passed one.
* @return - True if attribute was read successfully
*/
public boolean getPres(StyleAttribute attrib) throws SVGException
{
String presName = attrib.getName();
//Make sure we have a coresponding presentation attribute
StyleAttribute presAttr = (StyleAttribute)presAttribs.get(presName);
//Copy presentation value directly
attrib.setStringValue(presAttr == null ? "" : presAttr.getStringValue());
//Evalutate coresponding track, if one exists
TrackBase track = trackManager.getTrack(presName, AnimationElement.AT_XML);
if (track != null)
{
track.getValue(attrib, diagram.getUniverse().getCurTime());
return true;
}
//Return if we found presentation attribute
if (presAttr != null) return true;
return false;
}
/**
* @return the raw presentation value of this attribute. Ignores any
* modifications applied by style attributes or animation. Used by
* animations to determine the starting point to animate from
*/
public StyleAttribute getPresAbsolute(String styName)
{
//Check for local inline styles
return (StyleAttribute)presAttribs.get(styName);
}
static protected AffineTransform parseTransform(String val) throws SVGException
{
final Matcher matchExpression = Pattern.compile("\\w+\\([^)]*\\)").matcher("");
AffineTransform retXform = new AffineTransform();
matchExpression.reset(val);
while (matchExpression.find())
{
retXform.concatenate(parseSingleTransform(matchExpression.group()));
}
return retXform;
}
static public AffineTransform parseSingleTransform(String val) throws SVGException
{
final Matcher matchWord = Pattern.compile("[-.\\w]+").matcher("");
AffineTransform retXform = new AffineTransform();
matchWord.reset(val);
if (!matchWord.find())
{
//Return identity transformation if no data present (eg, empty string)
return retXform;
}
String function = matchWord.group().toLowerCase();
LinkedList termList = new LinkedList();
while (matchWord.find())
{
termList.add(matchWord.group());
}
double[] terms = new double[termList.size()];
Iterator it = termList.iterator();
int count = 0;
while (it.hasNext())
{
terms[count++] = XMLParseUtil.parseDouble((String)it.next());
}
//Calculate transformation
if (function.equals("matrix"))
{
retXform.setTransform(terms[0], terms[1], terms[2], terms[3], terms[4], terms[5]);
}
else if (function.equals("translate"))
{
retXform.setToTranslation(terms[0], terms[1]);
}
else if (function.equals("scale"))
{
if (terms.length > 1)
retXform.setToScale(terms[0], terms[1]);
else
retXform.setToScale(terms[0], terms[0]);
}
else if (function.equals("rotate"))
{
if (terms.length > 2)
retXform.setToRotation(Math.toRadians(terms[0]), terms[1], terms[2]);
else
retXform.setToRotation(Math.toRadians(terms[0]));
}
else if (function.equals("skewx"))
{
retXform.setToShear(Math.toRadians(terms[0]), 0.0);
}
else if (function.equals("skewy"))
{
retXform.setToShear(0.0, Math.toRadians(terms[0]));
}
else
{
throw new SVGException("Unknown transform type");
}
return retXform;
}
static protected float nextFloat(LinkedList l)
{
String s = (String)l.removeFirst();
return Float.parseFloat(s);
}
static protected PathCommand[] parsePathList(String list)
{
final Matcher matchPathCmd = Pattern.compile("([MmLlHhVvAaQqTtCcSsZz])|([-+]?((\\d*\\.\\d+)|(\\d+))([eE][-+]?\\d+)?)").matcher(list);
//Tokenize
LinkedList tokens = new LinkedList();
while (matchPathCmd.find())
{
tokens.addLast(matchPathCmd.group());
}
boolean defaultRelative = false;
LinkedList cmdList = new LinkedList();
char curCmd = 'Z';
while (tokens.size() != 0)
{
String curToken = (String)tokens.removeFirst();
char initChar = curToken.charAt(0);
if ((initChar >= 'A' && initChar <='Z') || (initChar >= 'a' && initChar <='z'))
{
curCmd = initChar;
}
else
{
tokens.addFirst(curToken);
}
PathCommand cmd = null;
switch (curCmd)
{
case 'M':
cmd = new MoveTo(false, nextFloat(tokens), nextFloat(tokens));
curCmd = 'L';
break;
case 'm':
cmd = new MoveTo(true, nextFloat(tokens), nextFloat(tokens));
curCmd = 'l';
break;
case 'L':
cmd = new LineTo(false, nextFloat(tokens), nextFloat(tokens));
break;
case 'l':
cmd = new LineTo(true, nextFloat(tokens), nextFloat(tokens));
break;
case 'H':
cmd = new Horizontal(false, nextFloat(tokens));
break;
case 'h':
cmd = new Horizontal(true, nextFloat(tokens));
break;
case 'V':
cmd = new Vertical(false, nextFloat(tokens));
break;
case 'v':
cmd = new Vertical(true, nextFloat(tokens));
break;
case 'A':
cmd = new Arc(false, nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens),
nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
nextFloat(tokens), nextFloat(tokens));
break;
case 'a':
cmd = new Arc(true, nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens),
nextFloat(tokens) == 1f, nextFloat(tokens) == 1f,
nextFloat(tokens), nextFloat(tokens));
break;
case 'Q':
cmd = new Quadratic(false, nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens), nextFloat(tokens));
break;
case 'q':
cmd = new Quadratic(true, nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens), nextFloat(tokens));
break;
case 'T':
cmd = new QuadraticSmooth(false, nextFloat(tokens), nextFloat(tokens));
break;
case 't':
cmd = new QuadraticSmooth(true, nextFloat(tokens), nextFloat(tokens));
break;
case 'C':
cmd = new Cubic(false, nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens), nextFloat(tokens));
break;
case 'c':
cmd = new Cubic(true, nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens), nextFloat(tokens));
break;
case 'S':
cmd = new CubicSmooth(false, nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens), nextFloat(tokens));
break;
case 's':
cmd = new CubicSmooth(true, nextFloat(tokens), nextFloat(tokens),
nextFloat(tokens), nextFloat(tokens));
break;
case 'Z':
case 'z':
cmd = new Terminal();
break;
default:
throw new RuntimeException("Invalid path element");
}
cmdList.add(cmd);
defaultRelative = cmd.isRelative;
}
PathCommand[] retArr = new PathCommand[cmdList.size()];
cmdList.toArray(retArr);
return retArr;
}
static protected GeneralPath buildPath(String text, int windingRule)
{
PathCommand[] commands = parsePathList(text);
int numKnots = 2;
for (int i = 0; i < commands.length; i++)
{
numKnots += commands[i].getNumKnotsAdded();
}
GeneralPath path = new GeneralPath(windingRule, numKnots);
BuildHistory hist = new BuildHistory();
for (int i = 0; i < commands.length; i++)
{
PathCommand cmd = commands[i];
cmd.appendPath(path, hist);
}
return path;
}
/**
* Updates all attributes in this diagram associated with a time event.
* Ie, all attributes with track information.
* @return - true if this node has changed state as a result of the time
* update
*/
abstract public boolean updateTime(double curTime) throws SVGException;
public int getNumChildren()
{
return children.size();
}
public SVGElement getChild(int i)
{
return (SVGElement)children.get(i);
}
}