//
// @(#)SVGUtils.java 1.00 4/1/2002
//
// Copyright 2002 Zachary DelProposto. All rights reserved.
// Use is subject to license terms.
//
//
// 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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
// Or from http://www.gnu.org/
//
package dip.gui.map;
import dip.world.Province;
//import com.dautelle.util.TypeFormat;
//import org.apache.batik.swing.svg.JSVGComponent;
//import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.util.RunnableQueue;
//import org.apache.batik.bridge.UpdateManager;
import org.apache.batik.dom.util.XLinkSupport;
import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGElement;
import org.w3c.dom.svg.SVGUseElement;
import java.text.DecimalFormat;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.awt.geom.AffineTransform;
import java.awt.*;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.swing.*;
/**
* Assorted utilities for altering the Batik SVG DOM.
* <p>
* The add/remove/replace methods are not only thread safe but also
* follow Batik guidelines for altering the DOM. If the SVGComponent
* has been set to dynamic prior to loading of the SVG document, then
* any DOM change will be rendered automatically.
* <p>
* Also note that for the title/descriptions setting methods, it would be
* faster to create a Title or Description SVG element and use add / remove /
* replace as appropriate. This could be an issue if there are frequent title
* or description changes.
*
*/
public class SVGUtils
{
/** Default floating-point format precision */
private final static float FLOAT_PRECISION = 0.1f;
/**
* Sets the title of an SVG element.
* <p>
* a null title is not valid.
*/
public static void setTitle(SVGDocument document, SVGElement element, String title)
{
Node titleNode = null;
Node textNode = null;
NodeList nl = element.getChildNodes();
for(int i=0; i<nl.getLength(); i++)
{
Node node = nl.item(i);
if(node.getNodeName() == SVGConstants.SVG_TITLE_TAG)
{
titleNode = node;
textNode = titleNode.getFirstChild();
break;
}
}
if(titleNode == null)
{
titleNode = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, SVGConstants.SVG_TITLE_TAG);
element.appendChild(titleNode);
textNode = document.createTextNode(title);
titleNode.appendChild(textNode);
}
else
{
textNode.setNodeValue(title);
}
}// setTitle()
/**
* Sets the description of an SVG element.
* <p>
* A null description is not valid.
*/
public static void setDescription(SVGDocument document, SVGElement element, String description)
{
Node descNode = null;
Node textNode = null;
NodeList nl = element.getChildNodes();
for(int i=0; i<nl.getLength(); i++)
{
Node node = nl.item(i);
if(node.getNodeName() == SVGConstants.SVG_TITLE_TAG)
{
descNode = node;
textNode = descNode.getFirstChild();
break;
}
}
if(descNode == null)
{
descNode = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, SVGConstants.SVG_DESC_TAG);
element.appendChild(descNode);
textNode = document.createTextNode(description);
descNode.appendChild(textNode);
}
else
{
textNode.setNodeValue(description);
}
}// setDescription()
/**
* Creates an SVG <use> element.
* <p>
* Allows insertion of a previously-defined <symbol> element.
* If id is null, no ID is used. If style is null, no style is used.
* <p>
* x,y are required.<br>
* SymbolSize will specify the width/height; if null, no width/height will be specified.
* <p>
*
*/
public static SVGUseElement createUseElement(SVGDocument document, String symbolName, String id, String attClass,
float x, float y, MapMetadata.SymbolSize symbolSize)
{
// prepend '#' to name, if required
if(symbolName.charAt(0) != '#')
{
StringBuffer sb = new StringBuffer(symbolName.length() + 1);
sb.append('#');
sb.append(symbolName);
symbolName = sb.toString();
}
SVGUseElement useElement = (SVGUseElement) document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI,
SVGConstants.SVG_USE_TAG);
useElement.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, "xlink:href", symbolName);
useElement.setAttributeNS(null, SVGConstants.SVG_X_ATTRIBUTE, floatToString(x));
useElement.setAttributeNS(null, SVGConstants.SVG_Y_ATTRIBUTE, floatToString(y));
if(symbolSize != null)
{
useElement.setAttributeNS(null, SVGConstants.SVG_WIDTH_ATTRIBUTE, symbolSize.getWidth());
useElement.setAttributeNS(null, SVGConstants.SVG_HEIGHT_ATTRIBUTE, symbolSize.getHeight());
}
if(id != null)
{
useElement.setAttributeNS(null, SVGConstants.SVG_ID_ATTRIBUTE, id);
}
if(attClass != null)
{
useElement.setAttributeNS(null, SVGConstants.SVG_CLASS_ATTRIBUTE, attClass);
}
return useElement;
}// createUseElement()
/*
* Adds an SVG element underneath parent element.
* <p>
* This does NOT check to see if the same element has already been added.
* <p>
* This method is threadsafe.
public static void addSVGElement(JSVGComponent svgComponent, final SVGElement parent, final SVGElement child)
{
RunnableQueue rq = svgComponent.getUpdateManager().getUpdateRunnableQueue();
rq.invokeLater(new Runnable() {
public void run()
{
parent.appendChild(child);
}// run()
});
}// addSVGElement()
*/
/*
* Removes the specific SVG element.
* <p>
* This method is threadsafe.
public static void removeSVGElement(JSVGComponent svgComponent, final SVGElement element)
{
RunnableQueue rq = svgComponent.getUpdateManager().getUpdateRunnableQueue();
rq.invokeLater(new Runnable() {
public void run()
{
element.getParentNode().removeChild(element);
}// run()
});
}// removeSVGElement()
*/
/*
* Replaces a specific SVG element with a new element
* <p>
* This method is threadsafe.
public static void replaceSVGElement(JSVGComponent svgComponent, final SVGElement oldElement, final SVGElement newElement)
{
RunnableQueue rq = svgComponent.getUpdateManager().getUpdateRunnableQueue();
rq.invokeLater(new Runnable() {
public void run()
{
oldElement.getParentNode().replaceChild(newElement, oldElement);
}// run()
});
}// replaceSVGElement()
*/
/**
* Given a list of objects, finds the first SVG tag that
* has an ID that matches the object. That element is stored in the
* returned HashMap, and can be retreived via the Object key
* <p>
* Null objects are ignored.
* <p>
* Supported Objects in the looklist are:
* <ul>
* <li>String
* <li>Object (via toString() method)
* <li>Province (checks all short names via getShortNames())
* </ul>
*
*/
public static Map tagFinderSVG(List lookList, Node root)
{
return tagFinderSVG(lookList, root, false);
}// tagFinderSVG
/** As above, but allows any SVG element to be returned */
public static Map tagFinderSVG(List lookList, Node root, boolean anySVGElement)
{
List list = new ArrayList(lookList);
Map map = new HashMap( (4 * lookList.size())/3 );
// recursively walk tree from root
nodeWalker(root, list, map, anySVGElement);
return map;
}// tagFinderSVG
/**
* Given a list of objects, finds the first SVG tag that
* has an ID that matches the object. That element is stored in the
* returned HashMap, and can be retreived via the Object key
* <p>
* The SVG elements found are put into the supplied java.util.Map object.
* <p>
* Null objects are ignored.
* <p>
* Supported Objects in the looklist are:
* <ul>
* <li>String
* <li>Object (via toString() method)
* <li>Province (checks all short names via getShortNames())
* </ul>
*
*/
public static void tagFinderSVG(Map map, List lookList, Node root)
{
tagFinderSVG(map, lookList, root, false);
}// tagFinderSVG
/** As above but allows any SVG element to be returned */
public static void tagFinderSVG(Map map, List lookList, Node root, boolean anySVGElement)
{
List list = new ArrayList(lookList);
// recursively walk tree from root
nodeWalker(root, list, map, anySVGElement);
}// tagFinderSVG
/**
* Returns all elements with IDs under the given root, as an array of SVGElement objects.
* Objects w/o IDs are ignored.
*
*/
public static SVGElement[] idFinderSVG(Node root)
{
List list = new ArrayList(150);
idNodeWalker(root, list, true);
return (SVGElement[]) list.toArray(new SVGElement[list.size()]);
}// idFinderSVG()
/**
* Walks the nodes of the SVG DOM, recursively.
* All non-G or non-SYMBOL elements are ignored, if anySVGElement flag is false
*
*/
private static void nodeWalker(Node node, List list, Map map, boolean anySVGElement)
{
if( node.getNodeType() == Node.ELEMENT_NODE
&& ((anySVGElement && node instanceof org.w3c.dom.svg.SVGElement)
|| (node.getNodeName() == SVGConstants.SVG_G_TAG || node.getNodeName() == SVGConstants.SVG_SYMBOL_TAG)) )
{
// check if the element has an ID attribute
if(node.hasAttributes())
{
NamedNodeMap attributes = node.getAttributes();
Node attrNode = attributes.getNamedItem(SVGConstants.SVG_ID_ATTRIBUTE); // was ATTR_ID
if(attrNode != null)
{
nodeChecker(attrNode, node, list, map);
}
}
}
// check if current node has any children
// if so, iterate through & recursively call this method
NodeList children = node.getChildNodes();
if(children != null)
{
for(int i=0; i<children.getLength(); i++)
{
nodeWalker(children.item(i), list, map, anySVGElement);
}
}
}// nodeWalker()
/**
* Walks the nodes of the SVG DOM, recursively.
* Looks for any ELEMENT with an ID value.
*
*/
private static void idNodeWalker(Node node, List list, boolean isRoot)
{
if(node.getNodeType() == Node.ELEMENT_NODE && !isRoot)
{
String id = ((Element) node).getAttribute(SVGConstants.SVG_ID_ATTRIBUTE);
if( !"".equals(id) )
{
list.add( (SVGElement) node );
}
}
// check if current node has any children
// if so, iterate through & recursively call this method
NodeList children = node.getChildNodes();
if(children != null)
{
for(int i=0; i<children.getLength(); i++)
{
idNodeWalker(children.item(i), list, false);
}
}
}// nodeWalker()
/**
* Checks if the current node ID matches the ID of any elements in the
* list. If it does, the element is added to map, and removed from the list.
*
*/
private static void nodeChecker(Node attributeNode, Node parentNode, List list, Map map)
{
String nodeValue = attributeNode.getNodeValue();
Iterator iter = list.iterator();
while(iter.hasNext())
{
Object obj = iter.next();
if(obj == null)
{
iter.remove();
}
else
{
if(obj instanceof Province)
{
// use getShortNames() to check against all short names
String[] provShortNames = ((Province) obj).getShortNames();
for(int i=0; i<provShortNames.length; i++)
{
if(nodeValue.equalsIgnoreCase(provShortNames[i]))
{
map.put(obj, parentNode);
iter.remove();
return;
}
}
}
else
{
// for String and Objects, just use toString()
if(nodeValue.equalsIgnoreCase(obj.toString()))
{
map.put(obj, parentNode);
iter.remove();
return;
}
}
}
}
}// nodeChecker()
/**
* Walks the DOM tree from root, until the first element with the same ID is found.
* Case Insensitive.
*/
public static Node findNodeWithID(Node node, String id)
{
if(node.getNodeType() == Node.ELEMENT_NODE)
{
// check if the current element has an ID attribute
if(node.hasAttributes())
{
NamedNodeMap attributes = node.getAttributes();
Node attrNode = attributes.getNamedItem(SVGConstants.SVG_ID_ATTRIBUTE); // was ATTR_ID
if(attrNode != null)
{
String nodeValue = attrNode.getNodeValue();
if(nodeValue.equalsIgnoreCase(id))
{
return node;
}
}
}
}
// check if current node has any children
// if so, iterate through & recursively call this method
NodeList children = node.getChildNodes();
if(children != null)
{
for(int i=0; i<children.getLength(); i++)
{
Node aNode = findNodeWithID(children.item(i), id);
if(aNode != null)
{
return aNode;
}
}
}
return null;
}// findNodeWithID()
/**
* Calculates the maximum possible stretching of a canvas, remaining
* inside the bounds of a given scrollPane.
*
* @param canvas the canvas to be fitted
*/
// public static void getBestFit(JSVGCanvas canvas) {
// // NOTE: we use the initial transform...
// AffineTransform iat = canvas.getInitialTransform();
// if(iat != null)
// {
// Dimension dim = canvas.getSize();
// java.awt.geom.Dimension2D docSize = canvas.getSVGDocumentSize();
//
// //System.out.println("viewport extents: (getBestFit()): "+dim);
//
// // find out if width or height is larger; we use that to scale.
// double scaleFactor = 0.0;
// if(docSize.getWidth() >= docSize.getHeight())
// {
// scaleFactor = dim.getWidth() / docSize.getWidth();
// }
// else
// {
// scaleFactor = dim.getHeight() / docSize.getHeight();
// }
//
// AffineTransform t = AffineTransform.getTranslateInstance(0,0);
// t.scale(scaleFactor, scaleFactor);
// t.concatenate(iat);
// canvas.setRenderingTransform(t);
// }
// }
/**
* Formats a Floating-Point value into a String,
* using the jDip default precision.
*/
public static String floatToString(float v)
{
return floatToSB(v).toString();
}// toString()
/**
* Formats a Floating-Point value into a StringBuffer,
* using the jDip default precision.
*/
public static void appendFloat(StringBuffer sb, float v)
{
sb.append(floatToSB(v));
}// appendFloat()
/**
* Internal append; assumes floats <= 8 digits.
* If ends in ".0", the ".0" is truncated.
*/
private static StringBuffer floatToSB(float v)
{
StringBuffer sb = new StringBuffer(8);
String s1 = new DecimalFormat("0.#").format(v);
sb.append(s1);
final int s = sb.length();
if(sb.charAt(s-1) == '0' && sb.charAt(s-2) == '.')
{
sb.delete(s-2,s);
}
return sb;
}// floatToSB()
}// class SVGUtils