package ika.geoexport; import java.awt.*; import java.awt.geom.*; import java.io.*; import org.w3c.dom.*; import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*; import ika.utils.*; import ika.geo.*; /** * 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. */ private boolean useCSSStyles = false; private static String SVGNAMESPACE = "http://www.w3.org/2000/svg"; private static String XLINKNAMESPACE = "http://www.w3.org/1999/xlink"; private 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"; } /** * 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 { // create a document DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.newDocument(); // 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) { Rectangle2D bounds = geoSet.getBounds2D(GeoObject.UNDEFINED_SCALE); // create the main svg element Element svg = (Element)document.createElement("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.createElement("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) { // add a description element Element desc = (Element)document.createElement("desc"); svgRootElement.appendChild(desc); desc.appendChild(document.createTextNode(buildDescription())); // convert GeoSet to SVG DOM writeGeoSet(geoSet, svgRootElement, document); } protected void writeGeoObject(GeoObject obj, Element parent, Document doc) { 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. */ private void writeGeoSet(GeoSet geoSet, Element parent, Document document) { 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.createElement("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) { Element el = geoTextToSVG(geoText, document); parent.appendChild(el); finish(geoText, document, el); } protected void writeGeoImage(GeoImage geoImage, Element parent, Document document) { 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.createElement("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.createElement("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) { // 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) { GeoPathIterator pi = geoPath.getIterator(); Element pathElement = writePathIterator(pi, geoPath.getVectorSymbol(), document); parent.appendChild(pathElement); finish(geoPath, document, pathElement); } protected Element writePathIterator(GeoPathIterator pi, VectorSymbol vectorSymbol, Document document) { String svgPath = convertPathIteratorToSVG(pi); Element pathElement = (Element)document.createElement("path"); if (vectorSymbol != null) { if (useCSSStyles) pathElement.setAttribute("style", symbolToCSS(vectorSymbol)); else { String strokeColor = vectorSymbol.isStroked() ? ColorUtils.colorToCSSString(vectorSymbol.getStrokeColor()) : "none"; pathElement.setAttribute("stroke", strokeColor); String fillColor = vectorSymbol.isFilled() ? ColorUtils.colorToCSSString(vectorSymbol.getFillColor()) : "none"; pathElement.setAttribute("fill", fillColor); if (vectorSymbol.isFillTransparent()) { String fillOpacity = Float.toString(vectorSymbol.getFillTransparency() / 255.f); pathElement.setAttribute("fill-opacity", fillOpacity); } double strokeWidth = dimToPageRoundedPx( vectorSymbol.getScaledStrokeWidth(getDisplayMapScale())); if (strokeWidth <= 0) strokeWidth = 1; pathElement.setAttribute("stroke-width", Double.toString(strokeWidth)); } } pathElement.setAttribute("d", svgPath); return pathElement; } private String convertPathIteratorToSVG(GeoPathIterator iterator){ StringBuffer str = new StringBuffer(); do { final int type = iterator.getInstruction(); switch (type) { case GeoPathModel.CLOSE: str.append(" z"); break; case GeoPathModel.MOVETO: str.append(" M"); str.append(xToPageRoundedPx(iterator.getX())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY())); break; case GeoPathModel.LINETO: str.append(" L"); str.append(xToPageRoundedPx(iterator.getX())); str.append(" "); str.append(yToPageRoundedPx(iterator.getY())); break; case GeoPathModel.QUADCURVETO: 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: 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(); } private String symbolToCSS(FontSymbol symbol){ StringBuffer str = new StringBuffer(); 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(); } private String buildDescription() { String appName = ""; try { Class info = Class.forName("ika.app.ApplicationInfo"); // name of the application appName = (String)info.getMethod("getApplicationName", (Class[])null).invoke(null, (Object[])null); } catch (Exception e) {} final String userName = System.getProperty("user.name"); java.util.Date date= java.util.Calendar.getInstance().getTime(); final String dateStr = java.text.DateFormat.getDateInstance().format(date); StringBuffer str = new StringBuffer(); if (userName.length() > 0) { str.append("Author:"); str.append(userName); str.append(" - "); } str.append("Generator:"); str.append(appName); str.append(" - "); str.append("Date:"); str.append(date); return str.toString(); } public boolean isUseCSSStyles() { return useCSSStyles; } public void setUseCSSStyles(boolean useCSSStyles) { this.useCSSStyles = useCSSStyles; } }