/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2015, Open Source Geospatial Foundation (OSGeo) * (C) 2001, Vivid Solutions * * 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. * * This is a port of the JTS WKTWriter to handle SQL MM types such as Curve. * We have subclassed so that our implementation can be used anywhere * a WKTWriter is needed. We would of tried for more code reuse except * the base class has reduced everything to private methods. */ package org.geotools.geometry.jts; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.List; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateSequence; 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.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geom.PrecisionModel; import com.vividsolutions.jts.io.WKTWriter; import com.vividsolutions.jts.util.Assert; /** * A fork of JTS own {@link WKTWriter} that can write curved geometries using SQL Multi-Media * Extension Well-Known Text * * @see WKTReader2 */ public class WKTWriter2 extends WKTWriter { /** * Generates the WKT for a <tt>POINT</tt> specified by a {@link Coordinate}. * * @param p0 the point coordinate * * @return the WKT */ public static String toPoint(Coordinate p0) { return "POINT ( " + p0.x + " " + p0.y + " )"; } /** * Generates the WKT for a <tt>LINESTRING</tt> specified by a {@link CoordinateSequence}. * * @param seq the sequence to write * * @return the WKT string */ public static String toLineString(CoordinateSequence seq) { StringBuffer buf = new StringBuffer(); buf.append("LINESTRING "); if (seq.size() == 0) buf.append(" EMPTY"); else { buf.append("("); for (int i = 0; i < seq.size(); i++) { if (i > 0) buf.append(", "); buf.append(seq.getX(i) + " " + seq.getY(i)); } buf.append(")"); } return buf.toString(); } /** * Generates the WKT for a <tt>LINESTRING</tt> specified by two {@link Coordinate}s. * * @param p0 the first coordinate * @param p1 the second coordinate * * @return the WKT */ public static String toLineString(Coordinate p0, Coordinate p1) { return "LINESTRING ( " + p0.x + " " + p0.y + ", " + p1.x + " " + p1.y + " )"; } private static int INDENT = 2; /** * 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(PrecisionModel precisionModel) { // the default number of decimal places is 16, which is sufficient // to accomodate the maximum precision of a double. int decimalPlaces = precisionModel.getMaximumSignificantDigits(); // specify decimal separator explicitly to avoid problems in other locales DecimalFormatSymbols symbols = new DecimalFormatSymbols(); symbols.setDecimalSeparator('.'); String fmtString = "0" + (decimalPlaces > 0 ? "." : "") + stringOfChar('#', decimalPlaces); return new DecimalFormat(fmtString, symbols); } /** * 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 */ public 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 int outputDimension = 2; private DecimalFormat formatter; private boolean isFormatted = false; private boolean useFormatting = false; private int level = 0; private int coordsPerLine = -1; private String indentTabStr = " "; /** * Creates a new WKTWriter with default settings */ public WKTWriter2() { } /** * Creates a writer that writes {@link Geometry}s with the given output dimension (2 or 3). If * the specified output dimension is 3, the Z value of coordinates will be written if it is * present (i.e. if it is not <code>Double.NaN</code>). * * @param outputDimension the coordinate dimension to output (2 or 3) */ public WKTWriter2(int outputDimension) { this.outputDimension = outputDimension; if (outputDimension < 2 || outputDimension > 3) throw new IllegalArgumentException("Invalid output dimension (must be 2 or 3)"); } /** * Sets whether the output will be formatted. * * @param isFormatted true if the output is to be formatted */ public void setFormatted(boolean isFormatted) { this.isFormatted = isFormatted; } /** * Sets the maximum number of coordinates per line written in formatted output. If the provided * coordinate number is <= 0, coordinates will be written all on one line. * * @param coordsPerLine the number of coordinates per line to output. */ public void setMaxCoordinatesPerLine(int coordsPerLine) { this.coordsPerLine = coordsPerLine; } /** * Sets the tab size to use for indenting. * * @param size the number of spaces to use as the tab string * @throws IllegalArgumentException if the size is non-positive */ public void setTab(int size) { if (size <= 0) throw new IllegalArgumentException("Tab count must be positive"); this.indentTabStr = stringOfChar(' ', size); } /** * Converts a <code>Geometry</code> to its Well-known Text representation. * * @param geometry a <code>Geometry</code> to process * @return a <Geometry Tagged Text> string (see the OpenGIS Simple Features Specification) */ public String write(Geometry geometry) { Writer sw = new StringWriter(); try { writeFormatted(geometry, isFormatted, sw); } catch (IOException ex) { Assert.shouldNeverReachHere(); } return sw.toString(); } /** * Converts a <code>Geometry</code> to its Well-known Text representation. * * @param geometry a <code>Geometry</code> to process */ public void write(Geometry geometry, Writer writer) throws IOException { writeFormatted(geometry, false, writer); } /** * Same as <code>write</code>, but with newlines and spaces to make the well-known text more * readable. * * @param geometry a <code>Geometry</code> to process * @return a <Geometry Tagged Text> string (see the OpenGIS Simple Features Specification), with * newlines and spaces */ public String writeFormatted(Geometry geometry) { Writer sw = new StringWriter(); try { writeFormatted(geometry, true, sw); } catch (IOException ex) { Assert.shouldNeverReachHere(); } return sw.toString(); } /** * Same as <code>write</code>, but with newlines and spaces to make the well-known text more * readable. * * @param geometry a <code>Geometry</code> to process */ public void writeFormatted(Geometry geometry, Writer writer) throws IOException { writeFormatted(geometry, true, writer); } /** * Converts a <code>Geometry</code> to its Well-known Text representation. * * @param geometry a <code>Geometry</code> to process */ private void writeFormatted(Geometry geometry, boolean useFormatting, Writer writer) throws IOException { this.useFormatting = useFormatting; formatter = createFormatter(geometry.getPrecisionModel()); appendGeometryTaggedText(geometry, 0, writer); } /** * Converts a <code>Geometry</code> to <Geometry Tagged Text> format, then appends it to * the writer. * * @param geometry the <code>Geometry</code> to process * @param writer the output writer to append to */ private void appendGeometryTaggedText(Geometry geometry, int level, Writer writer) throws IOException { indent(level, writer); if (geometry instanceof Point) { Point point = (Point) geometry; appendPointTaggedText(point.getCoordinate(), level, writer, point.getPrecisionModel()); } else if (geometry instanceof CircularString) { appendCircularStringTaggedText((CircularString) geometry, level, writer); } else if (geometry instanceof CircularRing) { appendCircularStringTaggedText((CircularRing) geometry, level, writer); } else if (geometry instanceof CompoundCurve) { appendCompoundCurveTaggedText((CompoundCurvedGeometry) geometry, level, writer); } else if (geometry instanceof CompoundRing) { appendCompoundCurveTaggedText((CompoundCurvedGeometry) geometry, level, writer); } else if (geometry instanceof LinearRing) { appendLinearRingTaggedText((LinearRing) geometry, level, writer); } else if (geometry instanceof LineString) { appendLineStringTaggedText((LineString) geometry, level, writer); } else if (geometry instanceof CurvePolygon) { appendCurvePolygonTaggedText((CurvePolygon) geometry, level, writer); } else if (geometry instanceof Polygon) { appendPolygonTaggedText((Polygon) geometry, level, writer); } else if (geometry instanceof MultiPoint) { appendMultiPointTaggedText((MultiPoint) geometry, level, writer); } else if (geometry instanceof MultiCurve) { appendMultiCurveTaggedText((MultiCurve) geometry, level, writer); } else if (geometry instanceof MultiLineString) { appendMultiLineStringTaggedText((MultiLineString) geometry, level, writer); } else if (geometry instanceof MultiSurface) { appendMultiSurfaceTaggedText((MultiSurface) geometry, level, writer); } else if (geometry instanceof MultiPolygon) { appendMultiPolygonTaggedText((MultiPolygon) geometry, level, writer); } else if (geometry instanceof GeometryCollection) { appendGeometryCollectionTaggedText((GeometryCollection) geometry, level, writer); } else { Assert.shouldNeverReachHere("Unsupported Geometry implementation:" + geometry.getClass()); } } /** * Converts a <code>Coordinate</code> to <Point Tagged Text> format, then appends it to * the writer. * * @param coordinate the <code>Coordinate</code> to process * @param writer the output writer to append to * @param precisionModel the <code>PrecisionModel</code> to use to convert from a precise * coordinate to an external coordinate */ private void appendPointTaggedText(Coordinate coordinate, int level, Writer writer, PrecisionModel precisionModel) throws IOException { writer.write("POINT "); appendPointText(coordinate, level, writer, precisionModel); } /** * Converts a <code>CircularString</code> to <CircularString Tagged Text> format, then * appends it to the writer. * * @param lineString the <code>LineString</code> to process * @param writer the output writer to append to */ private void appendCircularStringTaggedText(SingleCurvedGeometry circularString, int level, Writer writer) throws IOException { writer.write("CIRCULARSTRING "); appendControlPointText(circularString, level, false, writer); } /** * Converts a <code>LineString</code> to <LineString Tagged Text> format, then appends it * to the writer. * * @param lineString the <code>LineString</code> to process * @param writer the output writer to append to */ private void appendLineStringTaggedText(LineString lineString, int level, Writer writer) throws IOException { writer.write("LINESTRING "); appendLineStringText(lineString, level, false, writer); } /** * Converts a <code>LinearRing</code> to <LinearRing Tagged Text> format, then appends it * to the writer. * * @param linearRing the <code>LinearRing</code> to process * @param writer the output writer to append to */ private void appendLinearRingTaggedText(LinearRing linearRing, int level, Writer writer) throws IOException { writer.write("LINEARRING "); appendLineStringText(linearRing, level, false, writer); } /** * Converts a <code>Polygon</code> to <Polygon Tagged Text> format, then appends it to the * writer. * * @param polygon the <code>Polygon</code> to process * @param writer the output writer to append to */ private void appendPolygonTaggedText(Polygon polygon, int level, Writer writer) throws IOException { writer.write("POLYGON "); appendPolygonText(polygon, level, false, writer); } /** * Converts a <code>MultiPoint</code> to <MultiPoint Tagged Text> format, then appends it * to the writer. * * @param multipoint the <code>MultiPoint</code> to process * @param writer the output writer to append to */ private void appendMultiPointTaggedText(MultiPoint multipoint, int level, Writer writer) throws IOException { writer.write("MULTIPOINT "); appendMultiPointText(multipoint, level, writer); } /** * Converts a <code>MultiLineString</code> to <MultiLineString Tagged Text> format, then * appends it to the writer. * * @param multiLineString the <code>MultiLineString</code> to process * @param writer the output writer to append to */ private void appendMultiLineStringTaggedText(MultiLineString multiLineString, int level, Writer writer) throws IOException { writer.write("MULTILINESTRING "); appendMultiLineStringText(multiLineString, level, false, writer); } /** * Converts a <code>MultiPolygon</code> to <MultiPolygon Tagged Text> format, then appends * it to the writer. * * @param multiPolygon the <code>MultiPolygon</code> to process * @param writer the output writer to append to */ private void appendMultiPolygonTaggedText(MultiPolygon multiPolygon, int level, Writer writer) throws IOException { writer.write("MULTIPOLYGON "); appendMultiPolygonText(multiPolygon, level, writer); } /** * Converts a <code>GeometryCollection</code> to <GeometryCollection Tagged Text> format, * then appends it to the writer. * * @param geometryCollection the <code>GeometryCollection</code> to process * @param writer the output writer to append to */ private void appendGeometryCollectionTaggedText(GeometryCollection geometryCollection, int level, Writer writer) throws IOException { writer.write("GEOMETRYCOLLECTION "); appendGeometryCollectionText(geometryCollection, level, writer); } /** * Converts a <code>Coordinate</code> to <Point Text> format, then appends it to the * writer. * * @param coordinate the <code>Coordinate</code> to process * @param writer the output writer to append to * @param precisionModel the <code>PrecisionModel</code> to use to convert from a precise * coordinate to an external coordinate */ private void appendPointText(Coordinate coordinate, int level, Writer writer, PrecisionModel precisionModel) throws IOException { if (coordinate == null) { writer.write("EMPTY"); } else { writer.write("("); appendCoordinate(coordinate, writer); writer.write(")"); } } /** * Appends the i'th coordinate from the sequence to the writer * * @param seq the <code>CoordinateSequence</code> to process * @param i the index of the coordinate to write * @param writer the output writer to append to */ private void appendCoordinate(CoordinateSequence seq, int i, Writer writer) throws IOException { writer.write(writeNumber(seq.getX(i)) + " " + writeNumber(seq.getY(i))); if (outputDimension >= 3 && seq.getDimension() >= 3) { double z = seq.getOrdinate(i, 3); if (!Double.isNaN(z)) { writer.write(" "); writer.write(writeNumber(z)); } } } /** * Converts a <code>Coordinate</code> to <code><Point></code> format, then appends it to * the writer. * * @param coordinate the <code>Coordinate</code> to process * @param writer the output writer to append to */ private void appendControlPoint(double x, double y, Writer writer) throws IOException { writer.write(writeNumber(x) + " " + writeNumber(y)); } /** * Converts a <code>Coordinate</code> to <code><Point></code> format, then appends it to * the writer. * * @param coordinate the <code>Coordinate</code> to process * @param writer the output writer to append to */ private void appendCoordinate(Coordinate coordinate, Writer writer) throws IOException { writer.write(writeNumber(coordinate.x) + " " + writeNumber(coordinate.y)); if (outputDimension >= 3 && !Double.isNaN(coordinate.z)) { writer.write(" "); writer.write(writeNumber(coordinate.z)); } } /** * Converts a <code>double</code> to a <code>String</code>, not in scientific notation. * * @param d the <code>double</code> to convert * @return the <code>double</code> as a <code>String</code>, not in scientific notation */ private String writeNumber(double d) { return formatter.format(d); } /** * Converts a <code>LineString</code> to <LineString Text> format, then appends it to the * writer. * * @param lineString the <code>LineString</code> to process * @param writer the output writer to append to */ private void appendSequenceText(CoordinateSequence seq, int level, boolean doIndent, Writer writer) throws IOException { if (seq.size() == 0) { writer.write("EMPTY"); } else { if (doIndent) indent(level, writer); writer.write("("); for (int i = 0; i < seq.size(); i++) { if (i > 0) { writer.write(", "); if (coordsPerLine > 0 && i % coordsPerLine == 0) { indent(level + 1, writer); } } appendCoordinate(seq, i, writer); } writer.write(")"); } } /** * Writes out a {@link SingleCurvedGeometry} control points * * @param cg the <code>SingleCurvedGeometry</code> to process * @param writer the output writer to append to */ private void appendControlPointText(SingleCurvedGeometry cg, int level, boolean doIndent, Writer writer) throws IOException { if (((Geometry) cg).isEmpty()) { writer.write("EMPTY"); } else { if (doIndent) indent(level, writer); writer.write("("); double[] controlPoints = cg.getControlPoints(); for (int i = 0; i < controlPoints.length; i += 2) { if (i > 0) { writer.write(", "); if (coordsPerLine > 0 && i % coordsPerLine == 0) { indent(level + 1, writer); } } appendControlPoint(controlPoints[i], controlPoints[i + 1], writer); } writer.write(")"); } } /** * Converts a <code>LineString</code> to <LineString Text> format, then appends it to the * writer. * * @param lineString the <code>LineString</code> to process * @param writer the output writer to append to */ private void appendLineStringText(LineString lineString, int level, boolean doIndent, Writer writer) throws IOException { if (lineString.isEmpty()) { writer.write("EMPTY"); } else { if (doIndent) indent(level, writer); writer.write("("); for (int i = 0; i < lineString.getNumPoints(); i++) { if (i > 0) { writer.write(", "); if (coordsPerLine > 0 && i % coordsPerLine == 0) { indent(level + 1, writer); } } appendCoordinate(lineString.getCoordinateN(i), writer); } writer.write(")"); } } private void appendCurvePolygonTaggedText(CurvePolygon polygon, int level, Writer writer) throws IOException { writer.write("CURVEPOLYGON "); if (polygon.isEmpty()) { writer.write("EMPTY"); } else { writer.write("("); appendPotentialCurveText(polygon.getExteriorRing(), level, false, writer); for (int i = 0; i < polygon.getNumInteriorRing(); i++) { writer.write(", "); appendPotentialCurveText(polygon.getInteriorRingN(i), level + 1, true, writer); } writer.write(")"); } } private void appendMultiCurveTaggedText(MultiCurve mc, int level, Writer writer) throws IOException { writer.write("MULTICURVE "); if (mc.isEmpty()) { writer.write("EMPTY"); } else { writer.write("("); for (int i = 0; i < mc.getNumGeometries(); i++) { appendPotentialCurveText((LineString) mc.getGeometryN(i), level + 1, true, writer); if (i < mc.getNumGeometries() - 1) { writer.write(", "); } } writer.write(")"); } } private void appendMultiSurfaceTaggedText(MultiSurface ms, int level, Writer writer) throws IOException { writer.write("MULTISURFACE "); if (ms.isEmpty()) { writer.write("EMPTY"); } else { writer.write("("); for (int i = 0; i < ms.getNumGeometries(); i++) { appendPotentialCurvePolygonText((Polygon) ms.getGeometryN(i), level + 1, true, writer); if (i < ms.getNumGeometries() - 1) { writer.write(", "); } } writer.write(")"); } } /** * Converts a <code>Polygon</code> to <Polygon Text> format, then appends it to the * writer. * * @param polygon the <code>Polygon</code> to process * @param writer the output writer to append to */ private void appendPolygonText(Polygon polygon, int level, boolean indentFirst, Writer writer) throws IOException { if (polygon.isEmpty()) { writer.write("EMPTY"); } else { if (indentFirst) indent(level, writer); writer.write("("); appendLineStringText(polygon.getExteriorRing(), level, false, writer); for (int i = 0; i < polygon.getNumInteriorRing(); i++) { writer.write(", "); appendLineStringText(polygon.getInteriorRingN(i), level + 1, true, writer); } writer.write(")"); } } /** * Converts a <code>MultiPoint</code> to <MultiPoint Text> format, then appends it to the * writer. * * @param multiPoint the <code>MultiPoint</code> to process * @param writer the output writer to append to */ private void appendMultiPointText(MultiPoint multiPoint, int level, Writer writer) throws IOException { if (multiPoint.isEmpty()) { writer.write("EMPTY"); } else { writer.write("("); for (int i = 0; i < multiPoint.getNumGeometries(); i++) { if (i > 0) { writer.write(", "); indentCoords(i, level + 1, writer); } writer.write("("); appendCoordinate(((Point) multiPoint.getGeometryN(i)).getCoordinate(), writer); writer.write(")"); } writer.write(")"); } } private void appendCompoundCurveTaggedText(CompoundCurvedGeometry<LineString> multiLineString, int level, Writer writer) throws IOException { writer.write("COMPOUNDCURVE "); if (((Geometry) multiLineString).isEmpty()) { writer.write("EMPTY"); } else { int level2 = level; boolean doIndent = false; writer.write("("); List<LineString> components = multiLineString.getComponents(); for (int i = 0; i < components.size(); i++) { if (i > 0) { writer.write(", "); level2 = level + 1; doIndent = true; } LineString component = components.get(i); appendPotentialCurveText(component, level2, doIndent, writer); } writer.write(")"); } } private void appendPotentialCurveText(LineString component, int level, boolean doIndent, Writer writer) throws IOException { if (component instanceof SingleCurvedGeometry) { appendCircularStringTaggedText((SingleCurvedGeometry) component, level, writer); } else if (component instanceof CompoundCurvedGeometry) { appendCompoundCurveTaggedText((CompoundCurvedGeometry) component, level, writer); } else { appendLineStringText(component, level, doIndent, writer); } } private void appendPotentialCurvePolygonText(Polygon component, int level, boolean doIndent, Writer writer) throws IOException { if (component instanceof CurvePolygon) { appendCurvePolygonTaggedText((CurvePolygon) component, level, writer); } else { appendPolygonText(component, level, doIndent, writer); } } /** * Converts a <code>MultiLineString</code> to <MultiLineString Text> format, then appends * it to the writer. * * @param multiLineString the <code>MultiLineString</code> to process * @param writer the output writer to append to */ private void appendMultiLineStringText(MultiLineString multiLineString, int level, boolean indentFirst, Writer writer) throws IOException { if (multiLineString.isEmpty()) { writer.write("EMPTY"); } else { int level2 = level; boolean doIndent = indentFirst; writer.write("("); for (int i = 0; i < multiLineString.getNumGeometries(); i++) { if (i > 0) { writer.write(", "); level2 = level + 1; doIndent = true; } appendLineStringText((LineString) multiLineString.getGeometryN(i), level2, doIndent, writer); } writer.write(")"); } } /** * Converts a <code>MultiPolygon</code> to <MultiPolygon Text> format, then appends it to * the writer. * * @param multiPolygon the <code>MultiPolygon</code> to process * @param writer the output writer to append to */ private void appendMultiPolygonText(MultiPolygon multiPolygon, int level, Writer writer) throws IOException { if (multiPolygon.isEmpty()) { writer.write("EMPTY"); } else { int level2 = level; boolean doIndent = false; writer.write("("); for (int i = 0; i < multiPolygon.getNumGeometries(); i++) { if (i > 0) { writer.write(", "); level2 = level + 1; doIndent = true; } appendPolygonText((Polygon) multiPolygon.getGeometryN(i), level2, doIndent, writer); } writer.write(")"); } } /** * Converts a <code>GeometryCollection</code> to <GeometryCollectionText> format, then * appends it to the writer. * * @param geometryCollection the <code>GeometryCollection</code> to process * @param writer the output writer to append to */ private void appendGeometryCollectionText(GeometryCollection geometryCollection, int level, Writer writer) throws IOException { if (geometryCollection.isEmpty()) { writer.write("EMPTY"); } else { int level2 = level; writer.write("("); for (int i = 0; i < geometryCollection.getNumGeometries(); i++) { if (i > 0) { writer.write(", "); level2 = level + 1; } appendGeometryTaggedText(geometryCollection.getGeometryN(i), level2, writer); } writer.write(")"); } } private void indentCoords(int coordIndex, int level, Writer writer) throws IOException { if (coordsPerLine <= 0 || coordIndex % coordsPerLine != 0) return; indent(level, writer); } private void indent(int level, Writer writer) throws IOException { if (!useFormatting || level <= 0) return; writer.write("\n"); for (int i = 0; i < level; i++) { writer.write(indentTabStr); } } }