package jeql.command.io.kml; import java.io.IOException; import java.io.Writer; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * Writes a formatted string containing the KML * representation of a JTS Geometry. * Supports a user-defined line prefix and a user-defined maximum number of coordinates per line. * Indents components of Geometries to provide a nicely-formatted representation. */ public class KMLGeometryWriter { public static String writeGeometry(Geometry g, double z) { KMLGeometryWriter writer = new KMLGeometryWriter(); writer.setZ(z); return writer.write(g); } public static String writeGeometry(Geometry g, double z, int precision, boolean extrude, String altitudeMode) { KMLGeometryWriter writer = new KMLGeometryWriter(); writer.setZ(z); writer.setPrecision(precision); writer.setExtrude(extrude); writer.setAltitudeMode(altitudeMode); return writer.write(g); } /** * Returns a <code>String</code> of repeated characters. * *@param ch the character to repeat *@param count the number of times to repeat the character *@return a <code>String</code> of characters */ private static String stringOfChar(char ch, int count) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < count; i++) { buf.append(ch); } return buf.toString(); } private final int INDENT_SIZE = 2; // these could be make settable private static final String coordinateSeparator = ","; private static final String tupleSeparator = " "; private String linePrefix = null; private int maxCoordinatesPerLine = 5; private double zVal = Double.NaN; private boolean extrude = false; private String altitudeMode = null; private int precision = -1; private DecimalFormat numberFormatter = null; public KMLGeometryWriter() { } public void setLinePrefix(String linePrefix) { this.linePrefix = linePrefix; } public void setMaximumCoordinatesPerLine(int maxCoordinatesPerLine) { if (maxCoordinatesPerLine <= 0) { maxCoordinatesPerLine = 1; return; } this.maxCoordinatesPerLine = maxCoordinatesPerLine; } public void setZ(double zVal) { this.zVal = zVal; } public void setExtrude(boolean extrude) { this.extrude = extrude; } public void setAltitudeMode(String altitudeMode) { this.altitudeMode = altitudeMode; } public void setPrecision(int precision) { this.precision = precision; if (precision >= 0) numberFormatter = createFormatter(precision); } public String write(Geometry geom) { StringBuffer buf = new StringBuffer(); write(geom, buf); return buf.toString(); } public void write(Geometry geometry, Writer writer) throws IOException { writer.write(write(geometry)); } /** * Generates the GML representation of a JTS Geometry. * @param g Geometry to output */ public void write(Geometry g, StringBuffer buf) { writeGeometry(g, 0, buf); } /** * Generates the GML representation of a JTS Geometry. * @param g Geometry to output */ private void writeGeometry(Geometry g, int level, StringBuffer buf) { /* * order is important in this if-else list. * E.g. homogeneous collections need to come before GeometryCollection */ String attributes = ""; if (g instanceof Point) { writePoint((Point) g, attributes, level, buf); } else if (g instanceof LinearRing) { writeLinearRing((LinearRing) g, attributes, level, buf); } else if (g instanceof LineString) { writeLineString((LineString) g, attributes, level, buf); } else if (g instanceof Polygon) { writePolygon((Polygon) g, attributes, level, buf); } // KML only supports the MultiGeometry element /* else if (g instanceof MultiPoint) { writeMultiPoint((MultiPoint) g, attributes, level, buf); } else if (g instanceof MultiLineString) { writeMultiLineString((MultiLineString) g, attributes, level, buf); } else if (g instanceof MultiPolygon) { writeMultiPolygon((MultiPolygon) g, attributes, level, buf); } */ else if (g instanceof GeometryCollection) { writeGeometryCollection((GeometryCollection) g, attributes, level, buf); } // throw an error for an unknown type? } private void startLine(StringBuffer buf, int level, String text) { if (linePrefix != null) buf.append(linePrefix); buf.append(stringOfChar(' ', INDENT_SIZE * level)); buf.append(text); } private String geometryTag(String geometryName, String attributes) { StringBuffer buf = new StringBuffer(); buf.append("<"); buf.append(geometryName); if (attributes != null && attributes.length() > 0) { buf.append(" "); buf.append(attributes); } buf.append(">"); // this is cheesy... AND WRONG! (because these get written in geom sub-components too if (extrude) buf.append("\n <extrude>1</extrude>"); if (altitudeMode != null) buf.append("\n <altitudeMode>" + altitudeMode + "</altitudeMode>"); return buf.toString(); } //<Point><coordinates>1195156.78946687,382069.533723461</coordinates></Point> private void writePoint(Point p, String attributes, int level, StringBuffer buf) { startLine(buf, level, geometryTag("Point", attributes) + "\n"); write(new Coordinate[] { p.getCoordinate() }, level + 1, buf); startLine(buf, level, "</Point>\n"); } //<LineString><coordinates>1195123.37289257,381985.763974674 1195120.22369473,381964.660533343 1195118.14929823,381942.597718511</coordinates></LineString> private void writeLineString(LineString ls, String attributes, int level, StringBuffer buf) { startLine(buf, level, geometryTag("LineString", attributes) + "\n"); write(ls.getCoordinates(), level + 1, buf); startLine(buf, level, "</LineString>\n"); } //<LinearRing><coordinates>1226890.26761027,1466433.47430292 1226880.59239079,1466427.03208053...></coordinates></LinearRing> private void writeLinearRing(LinearRing lr, String attributes, int level, StringBuffer buf) { startLine(buf, level, geometryTag("LinearRing", attributes) + "\n"); //startLine(buf, level, " <tessellate>1</tessellate>\n"); write(lr.getCoordinates(), level + 1, buf); startLine(buf, level, "</LinearRing>\n"); } private void writePolygon(Polygon p, String attributes, int level, StringBuffer buf) { startLine(buf, level, geometryTag("Polygon", attributes) + "\n"); startLine(buf, level, " <outerBoundaryIs>\n"); writeLinearRing((LinearRing) p.getExteriorRing(), null, level + 1, buf); startLine(buf, level, " </outerBoundaryIs>\n"); for (int t = 0; t < p.getNumInteriorRing(); t++) { startLine(buf, level, " <innerBoundaryIs>\n"); writeLinearRing((LinearRing) p.getInteriorRingN(t), null, level + 1, buf); startLine(buf, level, " </innerBoundaryIs>\n"); } startLine(buf, level, "</Polygon>\n"); } /* private void writeMultiPoint(MultiPoint mp, String attributes, int level, StringBuffer buf) { startLine(buf, level, geometryTag("MultiGeometry", attributes) + "\n"); for (int t = 0; t < mp.getNumGeometries(); t++) { writePoint((Point) mp.getGeometryN(t), null, level + 1, buf); } startLine(buf, level, "</MultiGeometry>\n"); } private void writeMultiLineString(MultiLineString mls, String attributes, int level, StringBuffer buf) { startLine(buf, level, geometryTag("MultiGeometry", attributes) + "\n"); for (int t = 0; t < mls.getNumGeometries(); t++) { writeLineString((LineString) mls.getGeometryN(t), null, level + 1, buf); } startLine(buf, level, "</MultiGeometry>\n"); } private void writeMultiPolygon(MultiPolygon mp, String attributes, int level, StringBuffer buf) { startLine(buf, level, geometryTag("MultiGeometry", attributes) + "\n"); for (int t = 0; t < mp.getNumGeometries(); t++) { writePolygon((Polygon) mp.getGeometryN(t), null, level + 1, buf); } startLine(buf, level, "</MultiGeometry>\n"); } */ private void writeGeometryCollection(GeometryCollection gc, String attributes, int level, StringBuffer buf) { startLine(buf, level, "<MultiGeometry>\n"); for (int t = 0; t < gc.getNumGeometries(); t++) { writeGeometry(gc.getGeometryN(t), level + 1, buf); } startLine(buf, level, "</MultiGeometry>\n"); } /** * Takes a list of coordinates and converts it to GML.<br> * 2d and 3d aware. * Terminates the coordinate output with a newline. *@param cs array of coordinates */ private void write(Coordinate[] coords, int level, StringBuffer buf) { startLine(buf, level, "<coordinates>"); boolean isNewLine = false; for (int i = 0; i < coords.length; i++) { if (i > 0) { buf.append(tupleSeparator); } if (isNewLine) { startLine(buf, level, " "); isNewLine = false; } write(coords[i], buf); // break output lines to prevent them from getting too long if ((i + 1) % maxCoordinatesPerLine == 0 && i < coords.length - 1) { buf.append("\n"); isNewLine = true; } } buf.append("</coordinates>\n"); } private void write(Coordinate p, StringBuffer buf) { write(p.x, buf); buf.append(coordinateSeparator); write(p.y, buf); double z = p.z; // if altitude was specified directly, use it if (! Double.isNaN(zVal)) z = zVal; // only write if Z present // MD - is this right? Or should it always be written? if (! Double.isNaN(z)) { buf.append(coordinateSeparator); write(z, buf); } } private void write(double num, StringBuffer buf) { if (numberFormatter != null) buf.append(numberFormatter.format(num)); else buf.append(num); } /** * Creates the <code>DecimalFormat</code> used to write <code>double</code>s * with a sufficient number of decimal places. * *@param precisionModel the <code>PrecisionModel</code> used to determine * the number of decimal places to write. *@return a <code>DecimalFormat</code> that write <code>double</code> * s without scientific notation. */ private static DecimalFormat createFormatter(int precision) { // specify decimal separator explicitly to avoid problems in other locales DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator('.'); DecimalFormat format = new DecimalFormat("0." + stringOfChar('#', precision), symbols); format.setDecimalSeparatorAlwaysShown(false); return format; } }