/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2008, Open Source Geospatial Foundation (OSGeo) * * 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; * version 2.1 of the License. * * 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. */ package org.geotools.renderer.style; import java.awt.Component; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Level; import javax.swing.Icon; import org.apache.batik.bridge.BridgeContext; import org.apache.batik.bridge.DocumentLoader; import org.apache.batik.bridge.GVTBuilder; import org.apache.batik.bridge.UserAgent; import org.apache.batik.bridge.UserAgentAdapter; import org.apache.batik.dom.svg.SAXSVGDocumentFactory; import org.apache.batik.gvt.GraphicsNode; import org.apache.batik.util.XMLResourceDescriptor; import org.geotools.styling.Displacement; import org.geotools.styling.ExternalGraphic; import org.geotools.styling.Graphic; import org.geotools.util.SoftValueHashMap; import org.opengis.feature.Feature; import org.opengis.filter.expression.Expression; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * External graphic factory accepting an Expression that can be evaluated to a URL pointing to a SVG * file. The <code>format</code> must be <code>image/svg+xml</code>, thought for backwards * compatibility <code>image/svg-xml</code> and <code>image/svg</code> are accepted as well. * * @author Andrea Aime - TOPP * * @source $URL: * http://svn.osgeo.org/geotools/branches/2.6.x/modules/plugin/svg/src/main/java/org/geotools * /renderer/style/SVGGraphicFactory.java $ */ public class SVGGraphicFactory implements ExternalGraphicFactory { /** Parsed SVG glyphs cache */ static Map<URL, RenderableSVG> glyphCache = Collections.synchronizedMap(new SoftValueHashMap<URL, RenderableSVG>()); /** The possible mime types for SVG */ static final Set<String> formats = new HashSet<String>() { { add("image/svg"); add("image/svg-xml"); add("image/svg+xml"); } }; public Icon getIcon(Feature feature, Expression url, String format, int size) throws Exception { // check we do support the declared format if (format == null || !formats.contains(format.toLowerCase())) return null; // grab the url URL svgfile = url.evaluate(feature, URL.class); if (svgfile == null) throw new IllegalArgumentException( "The specified expression could not be turned into an URL"); // turn the svg into a document and cache results RenderableSVG svg = glyphCache.get(svgfile); if(svg == null) { String parser = XMLResourceDescriptor.getXMLParserClassName(); SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); Document doc = f.createDocument(url.toString()); svg = new RenderableSVG(doc); glyphCache.put(svgfile, svg); } return new SVGIcon(svg, size); } private static class SVGIcon implements Icon { private int width; private int height; private RenderableSVG svg; public SVGIcon(RenderableSVG svg, int size) { this.svg = svg; // defines target width and height for render, based on the SVG bounds // and the specified desired height (if height is not provided, then // SVG bounds are used) Rectangle2D bounds = svg.bounds; double targetWidth = bounds.getWidth(); double targetHeight = bounds.getHeight(); if (size > 0) { double shapeAspectRatio = (bounds.getHeight() > 0 && bounds.getWidth() > 0) ? bounds .getWidth() / bounds.getHeight() : 1.0; targetWidth = shapeAspectRatio * size; targetHeight = size; } this.width = (int) Math.round(targetWidth); this.height = (int) Math.round(targetHeight); } public int getIconHeight() { return height; } public int getIconWidth() { return width; } public void paintIcon(Component c, Graphics g, int x, int y) { svg.paint((Graphics2D) g, width, height, x, y); } } private static class RenderableSVG { Rectangle2D bounds; private GraphicsNode node; public RenderableSVG(Document doc) { this.node = getGraphicNode(doc); this.bounds = getSvgDocBounds(doc); if (bounds == null) bounds = node.getBounds(); } /** * Retrieves an SVG document's specified bounds. * * @param svgLocation * an URL that specifies the SVG. * @return a {@link Rectangle2D} with the corresponding bounds. If the SVG document does not * specify any bounds, then null is returned. * @throws IOException * */ private Rectangle2D getSvgDocBounds(Document doc) { NodeList list = doc.getElementsByTagName("svg"); Node svgNode = list.item(0); NamedNodeMap attrbiutes = svgNode.getAttributes(); Node widthNode = attrbiutes.getNamedItem("width"); Node heightNode = attrbiutes.getNamedItem("height"); if (widthNode != null && heightNode != null) { double width = Double.parseDouble(widthNode.getNodeValue()); double height = Double.parseDouble(heightNode.getNodeValue()); return new Rectangle2D.Double(0.0, 0.0, width, height); } return null; } /** * Retrieves a Batik {@link GraphicsNode} for a given SVG. * * @param svgLocation * an URL that specifies the SVG. * @return the corresponding GraphicsNode. * @throws IOException * @throws URISyntaxException */ private GraphicsNode getGraphicNode(Document doc) { // instantiates objects needed for building the node UserAgent userAgent = new UserAgentAdapter(); DocumentLoader loader = new DocumentLoader(userAgent); BridgeContext ctx = new BridgeContext(userAgent, loader); ctx.setDynamic(true); // creates node builder and builds node GVTBuilder builder = new GVTBuilder(); return builder.build(ctx, doc); } public void paint(Graphics2D g, int width, int height, int x, int y) { // saves the old transform; // (needs synchronizing since we will mess around with the node's transform) AffineTransform oldTransform = g.getTransform(); try { if (oldTransform == null) oldTransform = new AffineTransform(); AffineTransform transform = new AffineTransform(oldTransform); // adds scaling to the transform so that we respect the declared size transform.scale(width / bounds.getWidth(), height / bounds.getHeight()); g.setTransform(transform); node.paint(g); } finally { g.setTransform(oldTransform); } } } /** * Forcefully drops the SVG cache */ public void resetCache() { glyphCache.clear(); } }