package ika.geoexport; import ika.geo.*; import ika.utils.*; import java.awt.*; import java.awt.geom.*; import java.io.*; import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*; import org.w3c.dom.*; /** * Exporter for the SVG file format. */ public class SVGExporter extends VectorGraphicsExporter { /** * If useCSSStyles is true vector styles are written using CSS styles. * Otherwise attributes are used. Rendering of interactively changed CSS * styles seems to be slower. CSS should therefore not be used for * interactive maps that change symbolization attributes. CSS styles are not * recommended, see http://jwatt.org/svg/authoring/ and are not part of SVG * Tiny. */ protected boolean useCSSStyles = false; protected boolean writeCompactPath = false; protected static String SVGNAMESPACE = "http://www.w3.org/2000/svg"; protected static String XLINKNAMESPACE = "http://www.w3.org/1999/xlink"; protected static String XMLEVENTSNAMESPACE = "http://www.w3.org/2001/xml-events"; // private static String svgIdentifier = "-//W3C//DTD SVG 1.0//EN"; // private static String svgDTD = "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"; public SVGExporter() { } public String getFileFormatName() { return "SVG"; } public String getFileExtension() { return "svg"; } /** * Give derived classes an option to provide own Document. * * @return */ protected Document createDocument() throws ParserConfigurationException { // create a document DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); return builder.newDocument(); } /** * Exports a GeoSet to a new SVG file. * * @param geoSet The GeoSet to export. * @param outputStream The OutputStream that will be used to export to. * @throws java.io.IOException Throws an exception if export is not * possible. */ protected void write(GeoSet geoSet, OutputStream outputStream) throws IOException { try { Document document = createDocument(); // construct the SVG root element Element svgRootElement = createSVGRootElement(geoSet, document); document.appendChild(svgRootElement); // add content to SVG root element addSVGContent(geoSet, document, svgRootElement); // Prepare the output file OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "utf-8"); StreamResult result = new StreamResult(outputStreamWriter); // Write the DOM document to the file /* There is a bug in Java 1.5: XML output is not indented. To work around this bug: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6296446 (1)set the indent-number in the transformerfactory TransformerFactory tf = new TransformerFactory.newInstance(); tf.setAttribute("indent-number", new Integer(2)); (2)enable the indent in the transformer Transformer t = tf.newTransformer(); t.setOutputProperty(OutputKeys.INDENT, "yes"); (3)wrap the otuputstream with a writer (or bufferedwriter) t.transform(new DOMSource(doc), new StreamResult(new OutputStreamWriter(out, "utf-8")); You must do (3) to workaround a "buggy" behavior of the xml handling code. */ TransformerFactory tf = TransformerFactory.newInstance(); tf.setAttribute("indent-number", new Integer(2)); Transformer xformer = tf.newTransformer(); xformer.setOutputProperty(OutputKeys.INDENT, "yes"); Source source = new DOMSource(document); xformer.transform(source, result); // don't add doctype to SVG files. see http://jwatt.org/svg/authoring/ /* xformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, svgIdentifier); xformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, svgDTD); */ outputStreamWriter.flush(); } catch (Exception e) { String msg = e.getMessage() != null ? e.getMessage() : e.getClass().toString(); throw new IOException("Export to SVG not possible. " + msg); } } /** * Creates the top level SVG element. */ protected Element createSVGRootElement(GeoSet geoSet, Document document) { // create the main svg element Element svg = (Element) document.createElementNS(SVGNAMESPACE, "svg"); // specify a namespace prefix on the 'svg' element, which means that // SVG is the default namespace for all elements within the scope of // the svg element with the xmlns attribute: // See http://www.w3.org/TR/SVG11/struct.html#SVGElement // and http://jwatt.org/svg/authoring/ svg.setAttribute("xmlns", SVGNAMESPACE); svg.setAttribute("xmlns:xlink", XLINKNAMESPACE); svg.setAttribute("xmlns:ev", XMLEVENTSNAMESPACE); svg.setAttribute("version", "1.0"); svg.setAttribute("preserveAspectRatio", "xMinYMin"); final double wWC = pageFormat.getPageWidthWorldCoordinates(); final double hWC = pageFormat.getPageHeightWorldCoordinates(); final double w = dimToPageRoundedPx((float) wWC); final double h = dimToPageRoundedPx((float) hWC); svg.setAttribute("width", Double.toString(w)); svg.setAttribute("height", Double.toString(h)); // Define the viewBox. String viewBoxStr = "0 0 " + w + " " + h; svg.setAttribute("viewBox", viewBoxStr); return svg; } /** * The default implementation simply creates a g element. Derived classes * may overwrite this. */ protected Element createSVGGroupElement(GeoSet geoSet, Document document) { // don't write invisible or empty GeoSets if (!geoSet.hasVisibleGeoObjects()) { return null; } return document.createElementNS(SVGNAMESPACE, "g"); } protected void finish(GeoSet geoSet, Document doc, Element element) { } protected void finish(GeoPath geoPath, Document doc, Element element) { } protected void finish(GeoPoint geoPoint, Document doc, Element element) { } protected void finish(GeoText geoText, Document doc, Element element) { } protected void addSVGContent(GeoSet geoSet, Document document, Element svgRootElement) throws IOException { // add a description element appendDescription(svgRootElement, document); // convert GeoSet to SVG DOM writeGeoSet(geoSet, svgRootElement, document); } protected void writeGeoObject(GeoObject obj, Element parent, Document doc) throws IOException { if (obj instanceof GeoPath) { writeGeoPath((GeoPath) obj, parent, doc); } else if (obj instanceof GeoImage) { writeGeoImage((GeoImage) obj, parent, doc); } else if (obj instanceof GeoPoint) { writeGeoPoint((GeoPoint) obj, parent, doc); } else if (obj instanceof GeoText) { writeGeoText((GeoText) obj, parent, doc); } } /** * Converts a GeoSet to a SVG DOM. * * @param geoSet The GeoSet to convert. * @param parent The parent element that will contain the passed GeoSet. * @param document The DOM. */ protected void writeGeoSet(GeoSet geoSet, Element parent, Document document) throws IOException { Element g = createSVGGroupElement(geoSet, document); if (g == null) { return; } parent.appendChild(g); final int nbrObj = geoSet.getNumberOfChildren(); for (int i = 0; i < nbrObj; i++) { GeoObject obj = geoSet.getGeoObject(i); // only write visible elements if (obj.isVisible() == false) { continue; } if (obj instanceof GeoSet) { writeGeoSet((GeoSet) obj, g, document); } else { writeGeoObject(obj, g, document); } } finish(geoSet, document, g); } protected Element geoTextToSVG(GeoText geoText, Document document) { FontSymbol symbol = geoText.getFontSymbol(); final double x = xToPageRoundedPx((float) geoText.getVisualX(1. / getDisplayMapScale())); final double y = yToPageRoundedPx((float) geoText.getVisualY(1. / getDisplayMapScale())); Element text = document.createElementNS(SVGNAMESPACE, "text"); text.setAttribute("x", Double.toString(x)); text.setAttribute("y", Double.toString(y)); if (useCSSStyles) { text.setAttribute("style", symbolToCSS(symbol)); } else { Font font = symbol.getFont(); text.setAttribute("font-size", Integer.toString(symbol.getSize())); text.setAttribute("font-family", font.getFamily()); text.setAttribute("fill", "black"); switch (font.getStyle()) { case Font.PLAIN: text.setAttribute("font-style", "normal"); break; case Font.BOLD: text.setAttribute("font-weight", "bold"); break; case Font.ITALIC: text.setAttribute("font-style", "italic"); break; } if (symbol.isCenterHor()) { text.setAttribute("text-anchor", "middle"); } else { text.setAttribute("text-anchor", "start"); } if (symbol.isCenterVer()) { text.setAttribute("baseline-shift", "50%"); } } text.setAttribute("id", Long.toString(geoText.getID())); Node textNode = document.createTextNode(geoText.getText()); text.appendChild(textNode); return text; } protected void writeGeoText(GeoText geoText, Element parent, Document document) throws IOException { Element el = geoTextToSVG(geoText, document); parent.appendChild(el); finish(geoText, document, el); } protected void writeGeoImage(GeoImage geoImage, Element parent, Document document) throws IOException { Rectangle2D bounds = geoImage.getBounds2D(GeoObject.UNDEFINED_SCALE); String xStr = Double.toString(xToPageRoundedPx((float) bounds.getMinX())); String yStr = Double.toString(yToPageRoundedPx((float) bounds.getMaxY())); String wStr = Double.toString(dimToPageRoundedPx((float) bounds.getWidth())); String hStr = Double.toString(dimToPageRoundedPx((float) bounds.getHeight())); Element image = (Element) document.createElementNS(SVGNAMESPACE, "image"); image.setAttribute("x", xStr); image.setAttribute("y", yStr); image.setAttribute("width", wStr); image.setAttribute("height", hStr); image.setAttribute("xlink:href", geoImage.getURL().toExternalForm()); parent.appendChild(image); // add rectangle of the size of the image Element rect = (Element) document.createElementNS(SVGNAMESPACE, "rect"); rect.setAttribute("x", xStr); rect.setAttribute("y", yStr); rect.setAttribute("width", wStr); rect.setAttribute("height", hStr); rect.setAttribute("fill", "none"); rect.setAttribute("stroke", "blue"); rect.setAttribute("stroke-width", "1"); parent.appendChild(rect); } protected void writeGeoPoint(GeoPoint geoPoint, Element parent, Document document) throws IOException { // Unfortunately Illustrator CS does not support SVG symbols correctly. // Therefore don't write SVG symbols, but convert GeoPoints to // normal graphics. PointSymbol pointSymbol = geoPoint.getPointSymbol(); GeoPath geoPath = pointSymbol.getPointSymbol(getDisplayMapScale(), geoPoint.getX(), geoPoint.getY()); GeoPathIterator pi = geoPath.getIterator(); Element pathElement = writePathIterator(pi, pointSymbol, document); parent.appendChild(pathElement); finish(geoPoint, document, pathElement); } /** * Converts a GeoPath to a SVG path. * * @param geoPath The GeoPath to convert. * @param parent The parent element that will contain the passed GeoSet. * @param document The DOM. */ protected void writeGeoPath(GeoPath geoPath, Element parent, Document document) throws IOException { GeoPathIterator pi = geoPath.getIterator(); Element pathElement = writePathIterator(pi, geoPath.getVectorSymbol(), document); parent.appendChild(pathElement); finish(geoPath, document, pathElement); } protected void writeVectorSymbol(Element svgElement, VectorSymbol vectorSymbol) { if (vectorSymbol != null && svgElement != null) { if (useCSSStyles) { svgElement.setAttribute("style", symbolToCSS(vectorSymbol)); } else { String strokeColor = vectorSymbol.isStroked() ? ColorUtils.colorToCSSString(vectorSymbol.getStrokeColor()) : "none"; svgElement.setAttribute("stroke", strokeColor); String fillColor = vectorSymbol.isFilled() ? ColorUtils.colorToCSSString(vectorSymbol.getFillColor()) : "none"; svgElement.setAttribute("fill", fillColor); if (vectorSymbol.isFillTransparent()) { float alpha = Math.round(vectorSymbol.getFillTransparency() / 255f * 100) / 100f; svgElement.setAttribute("fill-opacity", Float.toString(alpha)); } double strokeWidth = vectorSymbol.getScaledStrokeWidth(getDisplayMapScale()); if (strokeWidth <= 0) { strokeWidth = 1; } svgElement.setAttribute("stroke-width", Double.toString(strokeWidth)); } } } protected Element writePathIterator(GeoPathIterator pi, VectorSymbol vectorSymbol, Document document) { String svgPath = convertPathIteratorToSVG(pi); Element pathElement = (Element) document.createElementNS(SVGNAMESPACE, "path"); writeVectorSymbol(pathElement, vectorSymbol); pathElement.setAttribute("d", svgPath); return pathElement; } private String convertPathIteratorToSVG(GeoPathIterator iterator) { StringBuilder str = new StringBuilder(); do { final int instruction = iterator.getInstruction(); switch (instruction) { case GeoPathModel.CLOSE: if (!writeCompactPath) { str.append(" "); } str.append("z"); break; case GeoPathModel.MOVETO: if (!writeCompactPath && str.length() > 0) { str.append(" "); } str.append("M"); str.append(xToPageRoundedPx(iterator.getX())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY())); break; case GeoPathModel.LINETO: if (!writeCompactPath) { str.append(" "); } str.append("L"); str.append(xToPageRoundedPx(iterator.getX())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY())); break; case GeoPathModel.QUADCURVETO: if (!writeCompactPath) { str.append(" "); } str.append("Q"); str.append(xToPageRoundedPx(iterator.getX())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY())); str.append(" "); str.append(xToPageRoundedPx(iterator.getX2())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY2())); break; case GeoPathModel.CURVETO: if (!writeCompactPath) { str.append(" "); } str.append("C"); str.append(xToPageRoundedPx(iterator.getX())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY())); str.append(" "); str.append(xToPageRoundedPx(iterator.getX2())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY2())); str.append(" "); str.append(xToPageRoundedPx(iterator.getX3())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY3())); break; } } while (iterator.next()); return str.toString(); } /** * Converts a VectorSymbol to a CSS style. * * @param symbol The VectorSymbol to convert. * @return A CSS formated string. */ public String symbolToCSS(VectorSymbol symbol) { StringBuffer str = new StringBuffer(); // fill if (symbol.isFilled()) { str.append("fill:"); str.append(ColorUtils.colorToCSSString(symbol.getFillColor())); str.append(";"); if (symbol.isFillTransparent()) { String fillOpacity = Float.toString(symbol.getFillTransparency() / 255.f); str.append("fill-opacity:"); str.append(fillOpacity); str.append(";"); } } else { str.append("fill:none;"); } // stroke if (symbol.isStroked()) { str.append("stroke:"); str.append(ColorUtils.colorToCSSString(symbol.getStrokeColor())); str.append(";"); str.append("stroke-width:"); double strokeWidth = dimToPageRoundedPx( symbol.getScaledStrokeWidth(getDisplayMapScale())); if (strokeWidth <= 0) { strokeWidth = 1; } str.append(strokeWidth); str.append(";"); if (symbol.isDashed()) { str.append("stroke-dasharray:"); str.append(dimToPageRoundedPx(symbol.getScaledDashLength(getDisplayMapScale()))); str.append(","); str.append(dimToPageRoundedPx(symbol.getScaledDashLength(getDisplayMapScale()))); str.append(";"); } } // stroke:none is default and therefore not needed. return str.toString(); } protected String symbolToCSS(FontSymbol symbol) { StringBuilder str = new StringBuilder(); str.append("font-size:"); str.append(/*this.transformDimRound*/(symbol.getSize())); Font font = symbol.getFont(); str.append(";font-family:"); // font names that consist of multiple words must be enclosed by '' String fontFamily = font.getFamily(); if (fontFamily.indexOf(" ") != -1) { fontFamily = "'" + fontFamily + "'"; } str.append(fontFamily); str.append(";fill:black;"); switch (font.getStyle()) { case Font.PLAIN: str.append("font-style:normal;"); break; case Font.BOLD: str.append("font-weight:bold;"); break; case Font.ITALIC: str.append("font-style:italic;"); break; } if (symbol.isCenterHor()) { str.append("text-anchor:middle;"); } else { str.append("text-anchor:start;"); } if (symbol.isCenterVer()) { str.append("baseline-shift:-50%;"); } return str.toString(); } protected void appendDescription(Element svgRootElement, Document document) { StringBuilder str = new StringBuilder(); String userName = getDocumentAuthor(); if (userName != null && userName.length() > 0) { str.append("Author:"); str.append(userName); str.append(" - "); } String appName = getApplicationName(); if (appName != null && appName.length() > 0) { str.append("Generator:"); str.append(appName); str.append(" - "); } str.append("Date:"); str.append(java.util.Calendar.getInstance().getTime()); // create a description element Element desc = (Element) document.createElementNS(SVGNAMESPACE, "desc"); desc.appendChild(document.createTextNode(str.toString())); // append description element svgRootElement.appendChild(desc); } protected void appendTitle(String title, Element svgRootElement) { Document doc = svgRootElement.getOwnerDocument(); Element el = (Element) (doc.createElementNS(SVGNAMESPACE, "title")); el.appendChild(doc.createTextNode(title)); svgRootElement.appendChild(el); } public boolean isUseCSSStyles() { return useCSSStyles; } public void setUseCSSStyles(boolean useCSSStyles) { this.useCSSStyles = useCSSStyles; } }