Class WKTWriter

   1 /*
   2  * Copyright (c) 2016 Vivid Solutions.
   3  *
   4  * All rights reserved. This program and the accompanying materials
   5  * are made available under the terms of the Eclipse Public License 2.0
   6  * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
   7  * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
   8  * and the Eclipse Distribution License is available at
   9  *
  10  * http://www.eclipse.org/org/documents/edl-v10.php.
  11  */
  12 package org.locationtech.jts.io;
  13  
  14  
  15 import java.io.IOException;
  16 import java.io.StringWriter;
  17 import java.io.Writer;
  18 import java.util.EnumSet;
  19  
  20 import org.locationtech.jts.geom.Coordinate;
  21 import org.locationtech.jts.geom.CoordinateSequence;
  22 import org.locationtech.jts.geom.CoordinateSequenceFilter;
  23 import org.locationtech.jts.geom.Geometry;
  24 import org.locationtech.jts.geom.GeometryCollection;
  25 import org.locationtech.jts.geom.LineString;
  26 import org.locationtech.jts.geom.LinearRing;
  27 import org.locationtech.jts.geom.MultiLineString;
  28 import org.locationtech.jts.geom.MultiPoint;
  29 import org.locationtech.jts.geom.MultiPolygon;
  30 import org.locationtech.jts.geom.Point;
  31 import org.locationtech.jts.geom.Polygon;
  32 import org.locationtech.jts.geom.PrecisionModel;
  33 import org.locationtech.jts.util.Assert;
  34  
  35 /**
  36  * Writes the Well-Known Text representation of a {@link Geometry}.
  37  * The Well-Known Text format is defined in the
  38  * OGC <a href="http://www.opengis.org/techno/specs.htm">
  39  * <i>Simple Features Specification for SQL</i></a>.
  40  * See {@link WKTReader} for a formal specification of the format syntax.
  41  * <p>
  42  * The <code>WKTWriter</code> outputs coordinates rounded to the precision
  43  * model. Only the maximum number of decimal places 
  44  * necessary to represent the ordinates to the required precision will be
  45  * output.
  46  * <p>
  47  * The SFS WKT spec does not define a special tag for {@link LinearRing}s.
  48  * Under the spec, rings are output as <code>LINESTRING</code>s.
  49  * In order to allow precisely specifying constructed geometries, 
  50  * JTS also supports a non-standard <code>LINEARRING</code> tag which is used 
  51  * to output LinearRings.
  52  *
  53  * @version 1.7
  54  * @see WKTReader
  55  */
  56 public class WKTWriter
  57 {
  58   /**
  59    * Generates the WKT for a <tt>POINT</tt>
  60    * specified by a {@link Coordinate}.
  61    *
  62    * @param p0 the point coordinate
  63    *
  64    * @return the WKT
  65    */
  66   public static String toPoint(Coordinate p0)
  67   {
  68     return WKTConstants.POINT + " ( " + format(p0) + " )";
  69   }
  70  
  71   /**
  72    * Generates the WKT for a <tt>LINESTRING</tt>
  73    * specified by a {@link CoordinateSequence}.
  74    *
  75    * @param seq the sequence to write
  76    *
  77    * @return the WKT string
  78    */
  79   public static String toLineString(CoordinateSequence seq)
  80   {
  81     StringBuilder buf = new StringBuilder();
  82     buf.append(WKTConstants.LINESTRING);
  83     buf.append(" ");
  84     if (seq.size() == 0)
  85       buf.append(WKTConstants.EMPTY);
  86     else {
  87       buf.append("(");
  88       for (int i = 0; i < seq.size(); i++) {
  89         if (i > 0)
  90           buf.append(", ");
  91         buf.append(format(seq.getX(i), seq.getY(i)));
  92       }
  93       buf.append(")");
  94     }
  95     return buf.toString();
  96   }
  97  
  98   /**
  99    * Generates the WKT for a <tt>LINESTRING</tt>
 100    * specified by a {@link CoordinateSequence}.
 101    *
 102    * @param coord the sequence to write
 103    *
 104    * @return the WKT string
 105    */
 106   public static String toLineString(Coordinate[] coord)
 107   {
 108     StringBuilder buf = new StringBuilder();
 109     buf.append(WKTConstants.LINESTRING);
 110     buf.append(" ");
 111     if (coord.length == 0)
 112       buf.append(WKTConstants.EMPTY);
 113     else {
 114       buf.append("(");
 115       for (int i = 0; i < coord.length; i++) {
 116         if (i > 0)
 117           buf.append(", ");
 118         buf.append(format(coord[i]));
 119       }
 120       buf.append(")");
 121     }
 122     return buf.toString();
 123   }
 124  
 125   /**
 126    * Generates the WKT for a <tt>LINESTRING</tt>
 127    * specified by two {@link Coordinate}s.
 128    *
 129    * @param p0 the first coordinate
 130    * @param p1 the second coordinate
 131    *
 132    * @return the WKT
 133    */
 134   public static String toLineString(Coordinate p0, Coordinate p1)
 135   {
 136     return WKTConstants.LINESTRING + " ( " + format(p0) + ", " + format(p1) + " )";
 137   }
 138  
 139   public static String format(Coordinate p) {
 140     return format(p.x, p.y);
 141   }
 142   
 143   private static String format(double x, double y) {
 144     return OrdinateFormat.DEFAULT.format(x) + " " + OrdinateFormat.DEFAULT.format(y);
 145   }
 146   
 147   private static final int INDENT = 2;
 148   private static final int OUTPUT_DIMENSION = 2;
 149  
 150   /**
 151    *  Creates the <code>DecimalFormat</code> used to write <code>double</code>s
 152    *  with a sufficient number of decimal places.
 153    *
 154    *@param  precisionModel  the <code>PrecisionModel</code> used to determine
 155    *      the number of decimal places to write.
 156    *@return                 a <code>DecimalFormat</code> that write <code>double</code>
 157    *      s without scientific notation.
 158    */
 159   private static OrdinateFormat createFormatter(PrecisionModel precisionModel) {
 160     return OrdinateFormat.create(precisionModel.getMaximumSignificantDigits());
 161   }
 162  
 163   /**
 164    *  Returns a <code>String</code> of repeated characters.
 165    *
 166    *@param  ch     the character to repeat
 167    *@param  count  the number of times to repeat the character
 168    *@return        a <code>String</code> of characters
 169    */
 170   private static String stringOfChar(char ch, int count) {
 171     StringBuilder buf = new StringBuilder(count);
 172     for (int i = 0; i < count; i++) {
 173       buf.append(ch);
 174     }
 175     return buf.toString();
 176   }
 177  
 178   /**
 179    * A filter implementation to test if a coordinate sequence actually has
 180    * meaningful values for an ordinate bit-pattern
 181    */
 182   private class CheckOrdinatesFilter implements CoordinateSequenceFilter {
 183  
 184     private final EnumSet<Ordinate> checkOrdinateFlags;
 185     private final EnumSet<Ordinate> outputOrdinates;
 186  
 187     /**
 188      * Creates an instance of this class
 189
 190      * @param checkOrdinateFlags the index for the ordinates to test.
 191      */
 192     private CheckOrdinatesFilter(EnumSet<Ordinate> checkOrdinateFlags) {
 193  
 194       this.outputOrdinates = EnumSet.of(Ordinate.X, Ordinate.Y);
 195       this.checkOrdinateFlags = checkOrdinateFlags;
 196     }
 197  
 198     /** @see org.locationtech.jts.geom.CoordinateSequenceFilter#isGeometryChanged */
 199     public void filter(CoordinateSequence seq, int i) {
 200  
 201       if (checkOrdinateFlags.contains(Ordinate.Z) && !outputOrdinates.contains(Ordinate.Z)) {
 202         if (!Double.isNaN(seq.getZ(i)))
 203           outputOrdinates.add(Ordinate.Z);
 204       }
 205  
 206       if (checkOrdinateFlags.contains(Ordinate.M) && !outputOrdinates.contains(Ordinate.M)) {
 207         if (!Double.isNaN(seq.getM(i)))
 208           outputOrdinates.add(Ordinate.M);
 209       }
 210     }
 211  
 212     /** @see org.locationtech.jts.geom.CoordinateSequenceFilter#isGeometryChanged */
 213     public boolean isGeometryChanged() {
 214       return false;
 215     }
 216  
 217     /** @see org.locationtech.jts.geom.CoordinateSequenceFilter#isDone */
 218     public boolean isDone() {
 219       return outputOrdinates.equals(checkOrdinateFlags);
 220     }
 221  
 222     /**
 223      * Gets the evaluated ordinate bit-pattern
 224      *
 225      * @return A bit-pattern of ordinates with valid values masked by {@link #checkOrdinateFlags}.
 226      */
 227     EnumSet<Ordinate> getOutputOrdinates() {
 228       return outputOrdinates;
 229     }
 230   }
 231  
 232   private EnumSet<Ordinate> outputOrdinates;
 233   private final int outputDimension;
 234   private PrecisionModel precisionModel = null;
 235   private OrdinateFormat ordinateFormat = null;
 236   private boolean isFormatted = false;
 237   private int coordsPerLine = -1;
 238   private String indentTabStr ;
 239  
 240   /**
 241    * Creates a new WKTWriter with default settings
 242    */
 243   public WKTWriter()
 244   {
 245     this(OUTPUT_DIMENSION);
 246   }
 247  
 248   /**
 249    * Creates a writer that writes {@link Geometry}s with
 250    * the given output dimension (2 to 4).
 251    * The output follows the following rules:
 252    * <ul>
 253    *   <li>If the specified <b>output dimension is 3</b> and the <b>z is measure flag
 254    *   is set to true</b>, the Z value of coordinates will be written if it is present
 255    * (i.e. if it is not <code>Double.NaN</code>)</li>
 256    *   <li>If the specified <b>output dimension is 3</b> and the <b>z is measure flag
 257    *   is set to false</b>, the Measure value of coordinates will be written if it is present
 258    * (i.e. if it is not <code>Double.NaN</code>)</li>
 259    *   <li>If the specified <b>output dimension is 4</b>, the Z value of coordinates will
 260    *   be written even if it is not present when the Measure value is present.The Measrue
 261    *   value of coordinates will be written if it is present
 262    * (i.e. if it is not <code>Double.NaN</code>)</li>
 263    * </ul>
 264    *
 265    * @param outputDimension the coordinate dimension to output (2 to 4)
 266    */
 267   public WKTWriter(int outputDimension) {
 268  
 269     setTab(INDENT);
 270     this.outputDimension = outputDimension;
 271  
 272     if (outputDimension < 2 || outputDimension > 4)
 273       throw new IllegalArgumentException("Invalid output dimension (must be 2 to 4)");
 274  
 275     this.outputOrdinates = EnumSet.of(Ordinate.X, Ordinate.Y);
 276     if (outputDimension > 2)
 277       outputOrdinates.add(Ordinate.Z);
 278     if (outputDimension > 3)
 279       outputOrdinates.add(Ordinate.M);
 280   }
 281  
 282   /**
 283    * Sets whether the output will be formatted.
 284    *
 285    * @param isFormatted true if the output is to be formatted
 286    */
 287   public void setFormatted(boolean isFormatted)
 288   {
 289     this.isFormatted = isFormatted;
 290   }
 291  
 292   /**
 293    * Sets the maximum number of coordinates per line
 294    * written in formatted output.
 295    * If the provided coordinate number is <= 0,
 296    * coordinates will be written all on one line.
 297    *
 298    * @param coordsPerLine the number of coordinates per line to output.
 299    */
 300   public void setMaxCoordinatesPerLine(int coordsPerLine)
 301   {
 302     this.coordsPerLine = coordsPerLine;
 303   }
 304  
 305   /**
 306    * Sets the tab size to use for indenting.
 307    *
 308    * @param size the number of spaces to use as the tab string
 309    * @throws IllegalArgumentException if the size is non-positive
 310    */
 311   public void setTab(int size)
 312   {
 313     if(size <= 0)
 314       throw new IllegalArgumentException("Tab count must be positive");
 315     this.indentTabStr = stringOfChar(' ', size);
 316   }
 317  
 318   /**
 319    * Sets the {@link Ordinate} that are to be written. Possible members are:
 320    * <ul>
 321    * <li>{@link Ordinate#X}</li>
 322    * <li>{@link Ordinate#Y}</li>
 323    * <li>{@link Ordinate#Z}</li>
 324    * <li>{@link Ordinate#M}</li>
 325    * </ul>
 326    * Values of {@link Ordinate#X} and {@link Ordinate#Y} are always assumed and not
 327    * particularly checked for.
 328    *
 329    * @param outputOrdinates A set of {@link Ordinate} values
 330    */
 331   public void setOutputOrdinates(EnumSet<Ordinate> outputOrdinates) {
 332  
 333     this.outputOrdinates.remove(Ordinate.Z);
 334     this.outputOrdinates.remove(Ordinate.M);
 335  
 336     if (this.outputDimension == 3) {
 337       if (outputOrdinates.contains(Ordinate.Z))
 338         this.outputOrdinates.add(Ordinate.Z);
 339       else if (outputOrdinates.contains(Ordinate.M))
 340         this.outputOrdinates.add(Ordinate.M);
 341     }
 342     if (this.outputDimension == 4) {
 343       if (outputOrdinates.contains(Ordinate.Z))
 344         this.outputOrdinates.add(Ordinate.Z);
 345       if (outputOrdinates.contains(Ordinate.M))
 346         this.outputOrdinates.add(Ordinate.M);
 347     }
 348   }
 349  
 350   /**
 351    * Gets a bit-pattern defining which ordinates should be
 352    * @return an ordinate bit-pattern
 353    * @see #setOutputOrdinates(EnumSet)
 354    */
 355   public EnumSet<Ordinate> getOutputOrdinates() {
 356     return this.outputOrdinates;
 357   }
 358  
 359  
 360    /**
 361    * Sets a {@link PrecisionModel} that should be used on the ordinates written.
 362    * <p>If none/{@code null} is assigned, the precision model of the {@link Geometry#getFactory()}
 363    * is used.</p>
 364    * <p>Note: The precision model is applied to all ordinate values, not just x and y.</p>
 365    * @param precisionModel
 366    *    the flag indicating if {@link Coordinate#z}/{} is actually a measure value.
 367    */
 368   public void setPrecisionModel(PrecisionModel precisionModel) {
 369     this.precisionModel = precisionModel;
 370     this.ordinateFormat = OrdinateFormat.create(precisionModel.getMaximumSignificantDigits());
 371   }
 372  
 373   /**
 374    *  Converts a <code>Geometry</code> to its Well-known Text representation.
 375    *
 376    *@param  geometry  a <code>Geometry</code> to process
 377    *@return           a <Geometry Tagged Text> string (see the OpenGIS Simple
 378    *      Features Specification)
 379    */
 380   public String write(Geometry geometry)
 381   {
 382     Writer sw = new StringWriter();
 383  
 384     try {
 385       writeFormatted(geometry, false, sw);
 386     }
 387     catch (IOException ex) {
 388       Assert.shouldNeverReachHere();
 389     }
 390     return sw.toString();
 391   }
 392  
 393   /**
 394    *  Converts a <code>Geometry</code> to its Well-known Text representation.
 395    *
 396    *@param  geometry  a <code>Geometry</code> to process
 397    */
 398   public void write(Geometry geometry, Writer writer)
 399     throws IOException
 400   {
 401     // write the geometry
 402     writeFormatted(geometry, isFormatted, writer);
 403   }
 404  
 405   /**
 406    *  Same as <code>write</code>, but with newlines and spaces to make the
 407    *  well-known text more readable.
 408    *
 409    *@param  geometry  a <code>Geometry</code> to process
 410    *@return           a <Geometry Tagged Text> string (see the OpenGIS Simple
 411    *      Features Specification), with newlines and spaces
 412    */
 413   public String writeFormatted(Geometry geometry)
 414   {
 415     Writer sw = new StringWriter();
 416     try {
 417       writeFormatted(geometry, true, sw);
 418     }
 419     catch (IOException ex) {
 420       Assert.shouldNeverReachHere();
 421     }
 422     return sw.toString();
 423   }
 424   /**
 425    *  Same as <code>write</code>, but with newlines and spaces to make the
 426    *  well-known text more readable.
 427    *
 428    *@param  geometry  a <code>Geometry</code> to process
 429    */
 430   public void writeFormatted(Geometry geometry, Writer writer)
 431     throws IOException
 432   {
 433     writeFormatted(geometry, true, writer);
 434   }
 435   /**
 436    *  Converts a <code>Geometry</code> to its Well-known Text representation.
 437    *
 438    *@param  geometry  a <code>Geometry</code> to process
 439    */
 440   private void writeFormatted(Geometry geometry, boolean useFormatting, Writer writer)
 441     throws IOException
 442   {
 443     OrdinateFormat formatter = getFormatter(geometry);
 444     // append the WKT
 445     appendGeometryTaggedText(geometry, useFormatting, writer, formatter);
 446   }
 447  
 448   private OrdinateFormat getFormatter(Geometry geometry) {
 449     // if present use the cached formatter
 450     if (ordinateFormat != null)
 451       return ordinateFormat;
 452     
 453     // no precision model was specified, so use the geometry's
 454     PrecisionModel pm = geometry.getPrecisionModel();
 455     OrdinateFormat formatter = createFormatter(pm);
 456     return formatter;
 457   }
 458  
 459   /**
 460    *  Converts a <code>Geometry</code> to <Geometry Tagged Text> format,
 461    *  then appends it to the writer.
 462    *
 463    * @param  geometry           the <code>Geometry</code> to process
 464    * @param  useFormatting      flag indicating that the output should be formatted
 465    * @param  writer             the output writer to append to
 466    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 467    *      from a precise coordinate to an external coordinate
 468    */
 469   private void appendGeometryTaggedText(Geometry geometry, boolean useFormatting, Writer writer,
 470                                         OrdinateFormat formatter)
 471     throws IOException
 472   {
 473     // evaluate the ordinates actually present in the geometry
 474     CheckOrdinatesFilter cof = new CheckOrdinatesFilter(this.outputOrdinates);
 475     geometry.apply(cof);
 476  
 477     // Append the WKT
 478     appendGeometryTaggedText(geometry, cof.getOutputOrdinates(), useFormatting,
 479             0, writer, formatter);
 480   }
 481   /**
 482    *  Converts a <code>Geometry</code> to <Geometry Tagged Text> format,
 483    *  then appends it to the writer.
 484    *
 485    * @param  geometry           the <code>Geometry</code> to process
 486    * @param  useFormatting      flag indicating that the output should be formatted
 487    * @param  level              the indentation level
 488    * @param  writer             the output writer to append to
 489    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 490    *      from a precise coordinate to an external coordinate
 491    */
 492   private void appendGeometryTaggedText(
 493           Geometry geometry, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 494           int level, Writer writer, OrdinateFormat formatter)
 495     throws IOException
 496  
 497   {
 498     indent(useFormatting, level, writer);
 499  
 500     if (geometry instanceof Point) {
 501       appendPointTaggedText((Point) geometry, outputOrdinates, useFormatting,
 502               level, writer, formatter);
 503     }
 504     else if (geometry instanceof LinearRing) {
 505       appendLinearRingTaggedText((LinearRing) geometry, outputOrdinates, useFormatting,
 506               level, writer, formatter);
 507     }
 508     else if (geometry instanceof LineString) {
 509       appendLineStringTaggedText((LineString) geometry, outputOrdinates, useFormatting,
 510               level, writer, formatter);
 511     }
 512     else if (geometry instanceof Polygon) {
 513       appendPolygonTaggedText((Polygon) geometry, outputOrdinates, useFormatting,
 514               level, writer, formatter);
 515     }
 516     else if (geometry instanceof MultiPoint) {
 517       appendMultiPointTaggedText((MultiPoint) geometry, outputOrdinates,
 518               useFormatting, level, writer, formatter);
 519     }
 520     else if (geometry instanceof MultiLineString) {
 521       appendMultiLineStringTaggedText((MultiLineString) geometry, outputOrdinates,
 522               useFormatting, level, writer, formatter);
 523     }
 524     else if (geometry instanceof MultiPolygon) {
 525       appendMultiPolygonTaggedText((MultiPolygon) geometry, outputOrdinates,
 526               useFormatting, level, writer, formatter);
 527     }
 528     else if (geometry instanceof GeometryCollection) {
 529       appendGeometryCollectionTaggedText((GeometryCollection) geometry, outputOrdinates,
 530               useFormatting, level, writer, formatter);
 531     }
 532     else {
 533       Assert.shouldNeverReachHere("Unsupported Geometry implementation:"
 534            + geometry.getClass());
 535     }
 536   }
 537  
 538   /**
 539    *  Converts a <code>Coordinate</code> to <Point Tagged Text> format,
 540    *  then appends it to the writer.
 541    *
 542    * @param  point           the <code>Point</code> to process
 543    * @param  useFormatting      flag indicating that the output should be formatted
 544    * @param  level              the indentation level
 545    * @param  writer             the output writer to append to
 546    * @param  formatter          the formatter to use when writing numbers
 547    */
 548   private void appendPointTaggedText(
 549           Point point, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 550           int level, Writer writer, OrdinateFormat formatter)
 551     throws IOException
 552   {
 553     writer.write(WKTConstants.POINT);
 554     writer.write(" ");
 555     appendOrdinateText(outputOrdinates, writer);
 556     appendSequenceText(point.getCoordinateSequence(), outputOrdinates, useFormatting,
 557             level, false, writer, formatter);
 558   }
 559  
 560   /**
 561    *  Converts a <code>LineString</code> to <LineString Tagged Text>
 562    *  format, then appends it to the writer.
 563    *
 564    * @param  lineString  the <code>LineString</code> to process
 565    * @param  useFormatting      flag indicating that the output should be formatted
 566    * @param  level              the indentation level
 567    * @param  writer             the output writer to append to
 568    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 569    *      from a precise coordinate to an external coordinate
 570    */
 571   private void appendLineStringTaggedText(
 572           LineString lineString, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 573           int level, Writer writer, OrdinateFormat formatter)
 574     throws IOException
 575   {
 576     writer.write(WKTConstants.LINESTRING);
 577     writer.write(" ");
 578     appendOrdinateText(outputOrdinates, writer);
 579     appendSequenceText(lineString.getCoordinateSequence(), outputOrdinates, useFormatting,
 580             level, false, writer, formatter);
 581   }
 582  
 583   /**
 584    *  Converts a <code>LinearRing</code> to <LinearRing Tagged Text>
 585    *  format, then appends it to the writer.
 586    *
 587    * @param  linearRing  the <code>LinearRing</code> to process
 588    * @param  useFormatting      flag indicating that the output should be formatted
 589    * @param  level              the indentation level
 590    * @param  writer             the output writer to append to
 591    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 592    *      from a precise coordinate to an external coordinate
 593    */
 594   private void appendLinearRingTaggedText(
 595           LinearRing linearRing, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 596           int level, Writer writer, OrdinateFormat formatter)
 597     throws IOException
 598   {
 599     writer.write(WKTConstants.LINEARRING);
 600     writer.write(" ");
 601     appendOrdinateText(outputOrdinates, writer);
 602     appendSequenceText(linearRing.getCoordinateSequence(), outputOrdinates, useFormatting,
 603             level, false, writer, formatter);
 604   }
 605  
 606   /**
 607    *  Converts a <code>Polygon</code> to <Polygon Tagged Text> format,
 608    *  then appends it to the writer.
 609    *
 610    * @param  polygon  the <code>Polygon</code> to process
 611    * @param  useFormatting      flag indicating that the output should be formatted
 612    * @param  level              the indentation level
 613    * @param  writer             the output writer to append to
 614    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 615    *      from a precise coordinate to an external coordinate
 616    */
 617   private void appendPolygonTaggedText(
 618           Polygon polygon, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 619           int level, Writer writer, OrdinateFormat formatter)
 620     throws IOException
 621   {
 622     writer.write(WKTConstants.POLYGON);
 623     writer.write(" ");
 624     appendOrdinateText(outputOrdinates, writer);
 625     appendPolygonText(polygon, outputOrdinates, useFormatting,
 626             level, false, writer, formatter);
 627   }
 628  
 629   /**
 630    *  Converts a <code>MultiPoint</code> to <MultiPoint Tagged Text>
 631    *  format, then appends it to the writer.
 632    *
 633    * @param  multipoint  the <code>MultiPoint</code> to process
 634    * @param  useFormatting      flag indicating that the output should be formatted
 635    * @param  level              the indentation level
 636    * @param  writer             the output writer to append to
 637    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 638    *      from a precise coordinate to an external coordinate
 639    */
 640   private void appendMultiPointTaggedText(MultiPoint multipoint, EnumSet<Ordinate> outputOrdinates,
 641                                           boolean useFormatting, int level, Writer writer,
 642                                           OrdinateFormat formatter)
 643     throws IOException
 644   {
 645     writer.write(WKTConstants.MULTIPOINT); 
 646     writer.write(" ");
 647     appendOrdinateText(outputOrdinates, writer);
 648     appendMultiPointText(multipoint, outputOrdinates, useFormatting, level, writer, formatter);
 649   }
 650  
 651   /**
 652    *  Converts a <code>MultiLineString</code> to <MultiLineString Tagged
 653    *  Text> format, then appends it to the writer.
 654    *
 655    * @param  multiLineString  the <code>MultiLineString</code> to process
 656    * @param  useFormatting      flag indicating that the output should be formatted
 657    * @param  level              the indentation level
 658    * @param  writer             the output writer to append to
 659    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 660    *      from a precise coordinate to an external coordinate
 661    */
 662   private void appendMultiLineStringTaggedText(
 663           MultiLineString multiLineString, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 664           int level, Writer writer, OrdinateFormat formatter)
 665     throws IOException
 666   {
 667     writer.write(WKTConstants.MULTILINESTRING);
 668     writer.write(" ");
 669     appendOrdinateText(outputOrdinates, writer);
 670     appendMultiLineStringText(multiLineString, outputOrdinates, useFormatting,
 671             level, /*false, */writer, formatter);
 672   }
 673  
 674   /**
 675    *  Converts a <code>MultiPolygon</code> to <MultiPolygon Tagged Text>
 676    *  format, then appends it to the writer.
 677    *
 678    * @param  multiPolygon  the <code>MultiPolygon</code> to process
 679    * @param  useFormatting      flag indicating that the output should be formatted
 680    * @param  level              the indentation level
 681    * @param  writer             the output writer to append to
 682    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 683    *      from a precise coordinate to an external coordinate
 684    */
 685   private void appendMultiPolygonTaggedText(
 686           MultiPolygon multiPolygon, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 687           int level, Writer writer, OrdinateFormat formatter)
 688     throws IOException
 689   {
 690     writer.write(WKTConstants.MULTIPOLYGON);
 691     writer.write(" ");
 692     appendOrdinateText(outputOrdinates, writer);
 693     appendMultiPolygonText(multiPolygon, outputOrdinates, useFormatting,
 694             level, writer, formatter);
 695   }
 696  
 697   /**
 698    *  Converts a <code>GeometryCollection</code> to <GeometryCollection
 699    *  Tagged Text> format, then appends it to the writer.
 700    *
 701    * @param  geometryCollection  the <code>GeometryCollection</code> to process
 702    * @param  useFormatting      flag indicating that the output should be formatted
 703    * @param  level              the indentation level
 704    * @param  writer             the output writer to append to
 705    * @param  formatter       the <code>DecimalFormatter</code> to use to convert
 706    *      from a precise coordinate to an external coordinate
 707    */
 708   private void appendGeometryCollectionTaggedText(
 709           GeometryCollection geometryCollection, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 710           int level, Writer writer, OrdinateFormat formatter)
 711     throws IOException
 712   {
 713     writer.write(WKTConstants.GEOMETRYCOLLECTION);
 714     writer.write(" ");
 715     appendOrdinateText(outputOrdinates, writer);
 716     appendGeometryCollectionText(geometryCollection, outputOrdinates,
 717             useFormatting, level, writer, formatter);
 718   }
 719  
 720    /**
 721    * Appends the i'th coordinate from the sequence to the writer
 722    * <p>If the {@code seq} has coordinates that are {@link double.NAN}, these are not written, even though
 723    * {@link #outputDimension} suggests this.
 724    *
 725    * @param  seq        the <code>CoordinateSequence</code> to process
 726    * @param  i          the index of the coordinate to write
 727    * @param  writer     the output writer to append to
 728    * @param  formatter  the formatter to use for writing ordinate values
 729    */
 730   private void appendCoordinate(
 731           CoordinateSequence seq, EnumSet<Ordinate> outputOrdinates, int i,
 732           Writer writer, OrdinateFormat formatter)
 733       throws IOException
 734   {
 735     writer.write(writeNumber(seq.getX(i), formatter) + " " +
 736             writeNumber(seq.getY(i), formatter));
 737  
 738     if (outputOrdinates.contains(Ordinate.Z)) {
 739       writer.write(" ");
 740       writer.write(writeNumber(seq.getZ(i), formatter));
 741     }
 742  
 743     if (outputOrdinates.contains(Ordinate.M)) {
 744       writer.write(" ");
 745       writer.write(writeNumber(seq.getM(i), formatter));
 746     }
 747   }
 748  
 749   /**
 750    *  Converts a <code>double</code> to a <code>String</code>, not in scientific
 751    *  notation.
 752    *
 753    *@param  d  the <code>double</code> to convert
 754    *@return    the <code>double</code> as a <code>String</code>, not in
 755    *      scientific notation
 756    */
 757   private static String writeNumber(double d, OrdinateFormat formatter) {
 758     return formatter.format(d);
 759   }
 760  
 761   /**
 762    * Appends additional ordinate information. This function may
 763    * <ul>
 764    *   <li>append 'Z' if in {@code outputOrdinates} the
 765    *   {@link Ordinate#Z} value is included
 766    *   </li>
 767    *   <li>append 'M' if in {@code outputOrdinates} the
 768    *   {@link Ordinate#M} value is included
 769    *   </li>
 770    *   <li> append 'ZM' if in {@code outputOrdinates} the
 771    *   {@link Ordinate#Z} and
 772    *   {@link Ordinate#M} values are included
 773    *   </li>
 774    * </ul>
 775    *
 776    * @param outputOrdinates  a bit-pattern of ordinates to write.
 777    * @param writer         the output writer to append to.
 778    * @throws IOException   if an error occurs while using the writer.
 779    */
 780   private void appendOrdinateText(EnumSet<Ordinate> outputOrdinates, Writer writer) throws IOException {
 781  
 782     if (outputOrdinates.contains(Ordinate.Z))
 783       writer.append(WKTConstants.Z);
 784     if (outputOrdinates.contains(Ordinate.M))
 785       writer.append(WKTConstants.M);
 786   }
 787  
 788   /**
 789    *  Appends all members of a <code>CoordinateSequence</code> to the stream. Each {@code Coordinate} is separated from
 790    *  another using a colon, the ordinates of a {@code Coordinate} are separated by a space.
 791    *
 792    * @param  seq             the <code>CoordinateSequence</code> to process
 793    * @param  useFormatting   flag indicating that
 794    * @param  level           the indentation level
 795    * @param  indentFirst     flag indicating that the first {@code Coordinate} of the sequence should be indented for
 796    *                         better visibility
 797    * @param  writer          the output writer to append to
 798    * @param  formatter       the formatter to use for writing ordinate values.
 799    */
 800   private void appendSequenceText(CoordinateSequence seq, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 801                                   int level, boolean indentFirst, Writer writer, OrdinateFormat formatter)
 802     throws IOException
 803   {
 804     if (seq.size() == 0) {
 805       writer.write(WKTConstants.EMPTY);
 806     }
 807     else {
 808       if (indentFirst) indent(useFormatting, level, writer);
 809       writer.write("(");
 810       for (int i = 0; i < seq.size(); i++) {
 811         if (i > 0) {
 812           writer.write(", ");
 813           if (coordsPerLine > 0
 814               && i % coordsPerLine == 0) {
 815             indent(useFormatting, level + 1, writer);
 816           }
 817         }
 818         appendCoordinate(seq, outputOrdinates, i, writer, formatter);
 819       }
 820       writer.write(")");
 821     }
 822   }
 823  
 824   /**
 825    *  Converts a <code>Polygon</code> to <Polygon Text> format, then
 826    *  appends it to the writer.
 827    *
 828    * @param  polygon         the <code>Polygon</code> to process
 829    * @param  useFormatting   flag indicating that
 830    * @param  level           the indentation level
 831    * @param  indentFirst     flag indicating that the first {@code Coordinate} of the sequence should be indented for
 832    *                         better visibility
 833    * @param  writer          the output writer to append to
 834    * @param  formatter       the formatter to use for writing ordinate values.
 835    */
 836   private void appendPolygonText(
 837           Polygon polygon, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 838           int level, boolean indentFirst, Writer writer, OrdinateFormat formatter)
 839     throws IOException
 840   {
 841     if (polygon.isEmpty()) {
 842       writer.write(WKTConstants.EMPTY);
 843     }
 844     else {
 845       if (indentFirst) indent(useFormatting, level, writer);
 846       writer.write("(");
 847       appendSequenceText(polygon.getExteriorRing().getCoordinateSequence(), outputOrdinates,
 848               useFormatting, level, false, writer, formatter);
 849       for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
 850         writer.write(", ");
 851         appendSequenceText(polygon.getInteriorRingN(i).getCoordinateSequence(), outputOrdinates,
 852               useFormatting,level + 1,true, writer, formatter);
 853       }
 854       writer.write(")");
 855     }
 856   }
 857  
 858   /**
 859    *  Converts a <code>MultiPoint</code> to <MultiPoint Text> format, then
 860    *  appends it to the writer.
 861    *
 862    * @param  multiPoint      the <code>MultiPoint</code> to process
 863    * @param  useFormatting   flag indicating that
 864    * @param  level           the indentation level
 865    * @param  writer          the output writer to append to
 866    * @param  formatter       the formatter to use for writing ordinate values.
 867    */
 868   private void appendMultiPointText(
 869           MultiPoint multiPoint, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 870           int level, Writer writer, OrdinateFormat formatter)
 871     throws IOException
 872   {
 873     if (multiPoint.isEmpty()) {
 874       writer.write(WKTConstants.EMPTY);
 875     }
 876     else {
 877       writer.write("(");
 878       for (int i = 0; i < multiPoint.getNumGeometries(); i++) {
 879         if (i > 0) {
 880           writer.write(", ");
 881           indentCoords(useFormatting, i, level + 1, writer);
 882         }
 883         appendSequenceText(((Point) multiPoint.getGeometryN(i)).getCoordinateSequence(),
 884                 outputOrdinates, useFormatting, level, false, writer, formatter);
 885      }
 886       writer.write(")");
 887     }
 888   }
 889  
 890   /**
 891    *  Converts a <code>MultiLineString</code> to <MultiLineString Text>
 892    *  format, then appends it to the writer.
 893    *
 894    * @param  multiLineString  the <code>MultiLineString</code> to process
 895    * @param  useFormatting    flag indicating that
 896    * @param  level            the indentation level
 897    * //@param  indentFirst      flag indicating that the first {@code Coordinate} of the sequence should be indented for
 898    * //                         better visibility
 899    * @param  writer           the output writer to append to
 900    * @param  formatter        the formatter to use for writing ordinate values.
 901    */
 902   private void appendMultiLineStringText(MultiLineString multiLineString, EnumSet<Ordinate> outputOrdinates,
 903            boolean useFormatting, int level, /*boolean indentFirst, */Writer writer, OrdinateFormat formatter)
 904     throws IOException
 905   {
 906     if (multiLineString.isEmpty()) {
 907       writer.write(WKTConstants.EMPTY);
 908     }
 909     else {
 910       int level2 = level;
 911       boolean doIndent = false;
 912       writer.write("(");
 913       for (int i = 0; i < multiLineString.getNumGeometries(); i++) {
 914         if (i > 0) {
 915           writer.write(", ");
 916           level2 = level + 1;
 917           doIndent = true;
 918         }
 919         appendSequenceText(((LineString) multiLineString.getGeometryN(i)).getCoordinateSequence(),
 920                 outputOrdinates, useFormatting, level2, doIndent, writer, formatter);
 921       }
 922       writer.write(")");
 923     }
 924   }
 925  
 926   /**
 927    *  Converts a <code>MultiPolygon</code> to <MultiPolygon Text> format,
 928    *  then appends it to the writer.
 929    *
 930    * @param  multiPolygon  the <code>MultiPolygon</code> to process
 931    * @param  useFormatting   flag indicating that
 932    * @param  level           the indentation level
 933    * @param  writer          the output writer to append to
 934    * @param  formatter       the formatter to use for writing ordinate values.
 935    */
 936   private void appendMultiPolygonText(
 937           MultiPolygon multiPolygon, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 938           int level, Writer writer, OrdinateFormat formatter)
 939     throws IOException
 940   {
 941     if (multiPolygon.isEmpty()) {
 942       writer.write(WKTConstants.EMPTY);
 943     }
 944     else {
 945       int level2 = level;
 946       boolean doIndent = false;
 947       writer.write("(");
 948       for (int i = 0; i < multiPolygon.getNumGeometries(); i++) {
 949         if (i > 0) {
 950           writer.write(", ");
 951           level2 = level + 1;
 952           doIndent = true;
 953         }
 954         appendPolygonText((Polygon) multiPolygon.getGeometryN(i), outputOrdinates,
 955                 useFormatting, level2, doIndent, writer, formatter);
 956       }
 957       writer.write(")");
 958     }
 959   }
 960  
 961   /**
 962    *  Converts a <code>GeometryCollection</code> to <GeometryCollectionText>
 963    *  format, then appends it to the writer.
 964    *
 965    * @param  geometryCollection  the <code>GeometryCollection</code> to process
 966    * @param  useFormatting   flag indicating that
 967    * @param  level           the indentation level
 968    * @param  writer          the output writer to append to
 969    * @param  formatter       the formatter to use for writing ordinate values.
 970    */
 971   private void appendGeometryCollectionText(
 972           GeometryCollection geometryCollection, EnumSet<Ordinate> outputOrdinates, boolean useFormatting,
 973           int level, Writer writer, OrdinateFormat formatter)
 974     throws IOException
 975   {
 976     if (geometryCollection.isEmpty()) {
 977       writer.write(WKTConstants.EMPTY);
 978     }
 979     else {
 980       int level2 = level;
 981       writer.write("(");
 982       for (int i = 0; i < geometryCollection.getNumGeometries(); i++) {
 983         if (i > 0) {
 984           writer.write(", ");
 985           level2 = level + 1;
 986         }
 987         appendGeometryTaggedText(geometryCollection.getGeometryN(i), outputOrdinates,
 988                 useFormatting, level2, writer, formatter);
 989       }
 990       writer.write(")");
 991     }
 992   }
 993  
 994   private void indentCoords(boolean useFormatting, int coordIndex,  int level, Writer writer)
 995     throws IOException
 996   {
 997     if (coordsPerLine <= 0
 998         || coordIndex % coordsPerLine != 0)
 999       return;
1000     indent(useFormatting, level, writer);
1001   }
1002  
1003   private void indent(boolean useFormatting, int level, Writer writer)
1004     throws IOException
1005   {
1006     if (! useFormatting || level <= 0)
1007       return;
1008     writer.write("\n");
1009     for (int i = 0; i < level; i++) {
1010       writer.write(indentTabStr);
1011     }
1012   }
1013 }
1014