Class WKTReader

   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.Reader;
  17 import java.io.StreamTokenizer;
  18 import java.io.StringReader;
  19 import java.util.ArrayList;
  20 import java.util.EnumSet;
  21 import java.util.Locale;
  22  
  23 import org.locationtech.jts.geom.Coordinate;
  24 import org.locationtech.jts.geom.CoordinateSequence;
  25 import org.locationtech.jts.geom.CoordinateSequenceFactory;
  26  
  27 import org.locationtech.jts.geom.Geometry;
  28 import org.locationtech.jts.geom.GeometryCollection;
  29 import org.locationtech.jts.geom.GeometryFactory;
  30 import org.locationtech.jts.geom.LineString;
  31 import org.locationtech.jts.geom.LinearRing;
  32 import org.locationtech.jts.geom.MultiLineString;
  33 import org.locationtech.jts.geom.MultiPoint;
  34 import org.locationtech.jts.geom.MultiPolygon;
  35 import org.locationtech.jts.geom.Point;
  36 import org.locationtech.jts.geom.Polygon;
  37 import org.locationtech.jts.geom.PrecisionModel;
  38 import org.locationtech.jts.geom.impl.CoordinateArraySequenceFactory;
  39 import org.locationtech.jts.util.Assert;
  40 import org.locationtech.jts.util.AssertionFailedException;
  41  
  42 /**
  43  * Converts a geometry in Well-Known Text format to a {@link Geometry}.
  44  * <p>
  45  * <code>WKTReader</code> supports
  46  * extracting <code>Geometry</code> objects from either {@link Reader}s or
  47  *  {@link String}s. This allows it to function as a parser to read <code>Geometry</code>
  48  *  objects from text blocks embedded in other data formats (e.g. XML). <P>
  49  * <p>
  50  *  A <code>WKTReader</code> is parameterized by a <code>GeometryFactory</code>,
  51  *  to allow it to create <code>Geometry</code> objects of the appropriate
  52  *  implementation. In particular, the <code>GeometryFactory</code>
  53  *  determines the <code>PrecisionModel</code> and <code>SRID</code> that is
  54  *  used. <P>
  55  *
  56  *  The <code>WKTReader</code> converts all input numbers to the precise
  57  *  internal representation.
  58  *
  59  * <h3>Notes:</h3>
  60  * <ul>
  61  * <li>Keywords are case-insensitive.
  62  * <li>The reader supports non-standard "LINEARRING" tags.
  63  * <li>The reader uses <tt>Double.parseDouble</tt> to perform the conversion of ASCII
  64  * numbers to floating point.  This means it supports the Java
  65  * syntax for floating point literals (including scientific notation).
  66  * </ul>
  67  *
  68  * <h3>Syntax</h3>
  69  * The following syntax specification describes the version of Well-Known Text
  70  * supported by JTS.
  71  * (The specification uses a syntax language similar to that used in
  72  * the C and Java language specifications.)
  73  * <p>
  74  * As of version 1.15, JTS can read (but not write) WKT Strings including Z, M or ZM
  75  * in the name of the geometry type (ex. POINT Z, LINESTRINGZM).
  76  * Note that it only makes the reader more flexible, but JTS could already read
  77  * 3D coordinates from WKT String and still can't read 4D coordinates.
  78  *
  79  * <blockquote><pre>
  80  * <i>WKTGeometry:</i> one of<i>
  81  *
  82  *       WKTPoint  WKTLineString  WKTLinearRing  WKTPolygon
  83  *       WKTMultiPoint  WKTMultiLineString  WKTMultiPolygon
  84  *       WKTGeometryCollection</i>
  85  *
  86  * <i>WKTPoint:</i> <b>POINT</b><i>[Dimension]</i> <b>( </b><i>Coordinate</i> <b>)</b>
  87  *
  88  * <i>WKTLineString:</i> <b>LINESTRING</b><i>[Dimension]</i> <i>CoordinateSequence</i>
  89  *
  90  * <i>WKTLinearRing:</i> <b>LINEARRING</b><i>[Dimension]</i> <i>CoordinateSequence</i>
  91  *
  92  * <i>WKTPolygon:</i> <b>POLYGON</b><i>[Dimension]</i> <i>CoordinateSequenceList</i>
  93  *
  94  * <i>WKTMultiPoint:</i> <b>MULTIPOINT</b><i>[Dimension]</i> <i>CoordinateSingletonList</i>
  95  *
  96  * <i>WKTMultiLineString:</i> <b>MULTILINESTRING</b><i>[Dimension]</i> <i>CoordinateSequenceList</i>
  97  *
  98  * <i>WKTMultiPolygon:</i>
  99  *         <b>MULTIPOLYGON</b><i>[Dimension]</i> <b>(</b> <i>CoordinateSequenceList {</i> , <i>CoordinateSequenceList }</i> <b>)</b>
 100  *
 101  * <i>WKTGeometryCollection: </i>
 102  *         <b>GEOMETRYCOLLECTION</b><i>[Dimension]</i> <b> (</b> <i>WKTGeometry {</i> , <i>WKTGeometry }</i> <b>)</b>
 103  *
 104  * <i>CoordinateSingletonList:</i>
 105  *         <b>(</b> <i>CoordinateSingleton {</i> <b>,</b> <i>CoordinateSingleton }</i> <b>)</b>
 106  *         | <b>EMPTY</b>
 107  *         
 108  * <i>CoordinateSingleton:</i>
 109  *         <b>(</b> <i>Coordinate</i> <b>)</b>
 110  *         | <b>EMPTY</b>
 111  *
 112  * <i>CoordinateSequenceList:</i>
 113  *         <b>(</b> <i>CoordinateSequence {</i> <b>,</b> <i>CoordinateSequence }</i> <b>)</b>
 114  *         | <b>EMPTY</b>
 115  *
 116  * <i>CoordinateSequence:</i>
 117  *         <b>(</b> <i>Coordinate {</i> , <i>Coordinate }</i> <b>)</b>
 118  *         | <b>EMPTY</b>
 119  *
 120  * <i>Coordinate:
 121  *         Number Number Number<sub>opt</sub></i>
 122  *
 123  * <i>Number:</i> A Java-style floating-point number (including <tt>NaN</tt>, with arbitrary case)
 124  *
 125  * <i>Dimension:</i>
 126  *         <b>Z</b>|<b> Z</b>|<b>M</b>|<b> M</b>|<b>ZM</b>|<b> ZM</b>
 127  *
 128  * </pre></blockquote>
 129  *
 130  *
 131  *@version 1.7
 132  * @see WKTWriter
 133  */
 134 public class WKTReader
 135 {
 136   private static final String COMMA = ",";
 137   private static final String L_PAREN = "(";
 138   private static final String R_PAREN = ")";
 139   private static final String NAN_SYMBOL = "NaN";
 140  
 141   private GeometryFactory geometryFactory;
 142   private CoordinateSequenceFactory csFactory;
 143   private static CoordinateSequenceFactory csFactoryXYZM = CoordinateArraySequenceFactory.instance();
 144   private PrecisionModel precisionModel;
 145  
 146   /**
 147    * Flag indicating that the old notation of coordinates in JTS
 148    * is supported.
 149    */
 150   private static final boolean ALLOW_OLD_JTS_COORDINATE_SYNTAX = true;
 151   private boolean isAllowOldJtsCoordinateSyntax = ALLOW_OLD_JTS_COORDINATE_SYNTAX;
 152  
 153   /**
 154    * Flag indicating that the old notation of MultiPoint coordinates in JTS
 155    * is supported.
 156    */
 157   private static final boolean ALLOW_OLD_JTS_MULTIPOINT_SYNTAX = true;
 158   private boolean isAllowOldJtsMultipointSyntax = ALLOW_OLD_JTS_MULTIPOINT_SYNTAX;
 159  
 160   /**
 161    * Creates a reader that creates objects using the default {@link GeometryFactory}.
 162    */
 163   public WKTReader() {
 164     this(new GeometryFactory());
 165   }
 166  
 167   /**
 168    *  Creates a reader that creates objects using the given
 169    *  {@link GeometryFactory}.
 170    *
 171    *@param  geometryFactory  the factory used to create <code>Geometry</code>s.
 172    */
 173   public WKTReader(GeometryFactory geometryFactory) {
 174     this.geometryFactory = geometryFactory;
 175     this.csFactory = geometryFactory.getCoordinateSequenceFactory();
 176     this.precisionModel = geometryFactory.getPrecisionModel();
 177   }
 178  
 179   /**
 180    * Sets a flag indicating, that coordinates may have 3 ordinate values even though no Z or M ordinate indicator
 181    * is present. The default value is {@link #ALLOW_OLD_JTS_COORDINATE_SYNTAX}.
 182    *
 183    * @param value a boolean value
 184    */
 185   public void setIsOldJtsCoordinateSyntaxAllowed(boolean value) {
 186     isAllowOldJtsCoordinateSyntax = value;
 187   }
 188  
 189   /**
 190    * Sets a flag indicating, that point coordinates in a MultiPoint geometry must not be enclosed in paren.
 191    * The default value is {@link #ALLOW_OLD_JTS_MULTIPOINT_SYNTAX}
 192    * @param value a boolean value
 193    */
 194   public void setIsOldJtsMultiPointSyntaxAllowed(boolean value) {
 195     isAllowOldJtsMultipointSyntax = value;
 196   }
 197  
 198   /**
 199    * Reads a Well-Known Text representation of a {@link Geometry}
 200    * from a {@link String}.
 201    *
 202    * @param wellKnownText
 203    *            one or more <Geometry Tagged Text> strings (see the OpenGIS
 204    *            Simple Features Specification) separated by whitespace
 205    * @return a <code>Geometry</code> specified by <code>wellKnownText</code>
 206    * @throws ParseException
 207    *             if a parsing problem occurs
 208    */
 209   public Geometry read(String wellKnownText) throws ParseException {
 210     StringReader reader = new StringReader(wellKnownText);
 211     try {
 212       return read(reader);
 213     }
 214     finally {
 215       reader.close();
 216     }
 217   }
 218  
 219   /**
 220    * Reads a Well-Known Text representation of a {@link Geometry}
 221    * from a {@link Reader}.
 222    *
 223    *@param  reader           a Reader which will return a <Geometry Tagged Text>
 224    *      string (see the OpenGIS Simple Features Specification)
 225    *@return                  a <code>Geometry</code> read from <code>reader</code>
 226    *@throws  ParseException  if a parsing problem occurs
 227    */
 228   public Geometry read(Reader reader) throws ParseException {
 229     StreamTokenizer tokenizer = createTokenizer(reader);
 230     try {
 231       return readGeometryTaggedText(tokenizer);
 232     }
 233     catch (IOException e) {
 234       throw new ParseException(e.toString());
 235     }
 236   }
 237  
 238   /**
 239    * Utility function to create the tokenizer
 240    * @param reader a reader
 241    *
 242    * @return a WKT Tokenizer.
 243    */
 244   private static StreamTokenizer createTokenizer(Reader reader) {
 245     StreamTokenizer tokenizer = new StreamTokenizer(reader);
 246     // set tokenizer to NOT parse numbers
 247     tokenizer.resetSyntax();
 248     tokenizer.wordChars('a''z');
 249     tokenizer.wordChars('A''Z');
 250     tokenizer.wordChars(128 + 32255);
 251     tokenizer.wordChars('0''9');
 252     tokenizer.wordChars('-''-');
 253     tokenizer.wordChars('+''+');
 254     tokenizer.wordChars('.''.');
 255     tokenizer.whitespaceChars(0' ');
 256     tokenizer.commentChar('#');
 257  
 258     return tokenizer;
 259   }
 260  
 261   /**
 262    * Reads a <code>Coordinate</Code> from a stream using the given {@link StreamTokenizer}.
 263    * <p>
 264    *   All ordinate values are read, but -depending on the {@link CoordinateSequenceFactory} of the
 265    *   underlying {@link GeometryFactory}- not necessarily all can be handled. Those are silently dropped.
 266    * </p>
 267    * @param tokenizer the tokenizer to use
 268    * @param ordinateFlags a bit-mask defining the ordinates to read.
 269    * @param tryParen a value indicating if a starting {@link #L_PAREN} should be probed.
 270    * @return a {@link CoordinateSequence} of length 1 containing the read ordinate values
 271    *
 272    *@throws  IOException     if an I/O error occurs
 273    *@throws  ParseException  if an unexpected token was encountered
 274    */
 275   private CoordinateSequence getCoordinate(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags, boolean tryParen)
 276           throws IOException, ParseException
 277   {
 278  
 279     boolean opened = false;
 280     if (tryParen && isOpenerNext(tokenizer) ) {
 281       tokenizer.nextToken();
 282       opened = true;
 283     }
 284  
 285     // create a sequence for one coordinate
 286     int offsetM = ordinateFlags.contains(Ordinate.Z) ? 1 : 0;
 287     CoordinateSequence sequence = csFactory.create(1, toDimension(ordinateFlags), ordinateFlags.contains(Ordinate.M) ? 1 : 0);
 288     sequence.setOrdinate(0, CoordinateSequence.X, precisionModel.makePrecise(getNextNumber(tokenizer)));
 289     sequence.setOrdinate(0, CoordinateSequence.Y, precisionModel.makePrecise(getNextNumber(tokenizer)));
 290  
 291     // additionally read other vertices
 292     if (ordinateFlags.contains(Ordinate.Z))
 293       sequence.setOrdinate(0, CoordinateSequence.Z, getNextNumber(tokenizer));
 294     if (ordinateFlags.contains(Ordinate.M))
 295       sequence.setOrdinate(0, CoordinateSequence.Z + offsetM, getNextNumber(tokenizer));
 296  
 297     if (ordinateFlags.size() == 2 && this.isAllowOldJtsCoordinateSyntax && isNumberNext(tokenizer)) {
 298       sequence.setOrdinate(0, CoordinateSequence.Z, getNextNumber(tokenizer));
 299     }
 300  
 301     // read close token if it was opened here
 302     if (opened) {
 303       getNextCloser(tokenizer);
 304     }
 305  
 306     return sequence;
 307   }
 308  
 309   /**
 310    * Reads a <code>Coordinate</Code> from a stream using the given {@link StreamTokenizer}.
 311    * <p>
 312    *   All ordinate values are read, but -depending on the {@link CoordinateSequenceFactory} of the
 313    *   underlying {@link GeometryFactory}- not necessarily all can be handled. Those are silently dropped.
 314    * </p>
 315    * <p>
 316    *
 317    * </p>
 318    * @param tokenizer the tokenizer to use
 319    * @param ordinateFlags a bit-mask defining the ordinates to read.
 320    * @return a {@link CoordinateSequence} of length 1 containing the read ordinate values
 321    *
 322    *@throws  IOException     if an I/O error occurs
 323    *@throws  ParseException  if an unexpected token was encountered
 324    */
 325   private CoordinateSequence getCoordinateSequence(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags)
 326           throws IOException, ParseException {
 327     if (getNextEmptyOrOpener(tokenizer).equals(WKTConstants.EMPTY))
 328       return this.csFactory.create(0, toDimension(ordinateFlags), ordinateFlags.contains(Ordinate.M) ? 1 : 0);
 329     
 330     ArrayList coordinates = new ArrayList();
 331     do {
 332       coordinates.add(getCoordinate(tokenizer, ordinateFlags, false));
 333     } while (getNextCloserOrComma(tokenizer).equals(COMMA));
 334  
 335     return mergeSequences(coordinates, ordinateFlags);  }
 336  
 337   /**
 338    * Reads a <code>CoordinateSequence</Code> from a stream using the given {@link StreamTokenizer}
 339    * for an old-style JTS MultiPoint (Point coordinates not enclosed in parentheses).
 340    * <p>
 341    * All ordinate values are read, but -depending on the {@link CoordinateSequenceFactory} of the
 342    * underlying {@link GeometryFactory}- not necessarily all can be handled. Those are silently dropped.
 343    * </p>
 344    * @param tokenizer the tokenizer to use
 345    * @param ordinateFlags a bit-mask defining the ordinates to read.
 346    * @param tryParen a value indicating if a starting {@link #L_PAREN} should be probed for each coordinate.
 347    * @param isReadEmptyOrOpener indicates if an opening paren or EMPTY should be scanned for
 348    * @return a {@link CoordinateSequence} of length 1 containing the read ordinate values
 349    *
 350    * @throws  IOException     if an I/O error occurs
 351    * @throws  ParseException  if an unexpected token was encountered
 352 S  */
 353   private CoordinateSequence getCoordinateSequenceOldMultiPoint(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags)
 354           throws IOException, ParseException {
 355  
 356     ArrayList coordinates = new ArrayList();
 357     do {
 358       coordinates.add(getCoordinate(tokenizer, ordinateFlags, true));
 359     } while (getNextCloserOrComma(tokenizer).equals(COMMA));
 360  
 361     return mergeSequences(coordinates, ordinateFlags);
 362   }
 363  
 364   /**
 365    * Computes the required dimension based on the given ordinate values.
 366    * It is assumed that {@link Ordinate#X} and {@link Ordinate#Y} are included.
 367    *
 368    * @param ordinateFlags the ordinate bit-mask
 369    * @return the number of dimensions required to store ordinates for the given bit-mask.
 370    */
 371   private int toDimension(EnumSet<Ordinate> ordinateFlags) {
 372     int dimension = 2;
 373     if (ordinateFlags.contains(Ordinate.Z))
 374       dimension++;
 375     if (ordinateFlags.contains(Ordinate.M))
 376       dimension++;
 377  
 378     if (dimension == 2 && this.isAllowOldJtsCoordinateSyntax)
 379       dimension++;
 380  
 381     return dimension;
 382   }
 383  
 384   /**
 385    * Merges an array of one-coordinate-{@link CoordinateSequence}s into one {@link CoordinateSequence}.
 386    *
 387    * @param sequences an array of coordinate sequences. Each sequence contains <b>exactly one</b> coordinate.
 388    * @param ordinateFlags a bit-mask of required ordinates.
 389    * @return a coordinate sequence containing all coordinate
 390    */
 391   private CoordinateSequence mergeSequences(ArrayList sequences, EnumSet<Ordinate> ordinateFlags) {
 392  
 393     // if the sequences array is empty or null create an empty sequence
 394     if (sequences == null || sequences.size() == 0)
 395       return csFactory.create(0, toDimension(ordinateFlags));
 396  
 397     if (sequences.size() == 1)
 398       return (CoordinateSequence) sequences.get(0);
 399  
 400     EnumSet<Ordinate> mergeOrdinates;
 401     if (this.isAllowOldJtsCoordinateSyntax && ordinateFlags.size() == 2) {
 402       mergeOrdinates = ordinateFlags.clone();
 403       for (int i = 0; i < sequences.size(); i++) {
 404         if (((CoordinateSequence)sequences.get(i)).hasZ()) {
 405           mergeOrdinates.add(Ordinate.Z);
 406           break;
 407         }
 408       }
 409     }
 410     else
 411       mergeOrdinates = ordinateFlags;
 412  
 413     // create and fill the result sequence
 414     CoordinateSequence sequence = this.csFactory.create(sequences.size(), toDimension(mergeOrdinates),
 415             mergeOrdinates.contains(Ordinate.M) ? 1 : 0);
 416  
 417     int offsetM = CoordinateSequence.Z + (mergeOrdinates.contains(Ordinate.Z) ? 1 : 0);
 418     for (int i = 0; i < sequences.size(); i++) {
 419       CoordinateSequence item = (CoordinateSequence)sequences.get(i);
 420       sequence.setOrdinate(i, CoordinateSequence.X, item.getOrdinate(0, CoordinateSequence.X));
 421       sequence.setOrdinate(i, CoordinateSequence.Y, item.getOrdinate(0, CoordinateSequence.Y));
 422       if (mergeOrdinates.contains(Ordinate.Z))
 423         sequence.setOrdinate(i, CoordinateSequence.Z, item.getOrdinate(0, CoordinateSequence.Z));
 424       if (mergeOrdinates.contains(Ordinate.M))
 425         sequence.setOrdinate(i, offsetM, item.getOrdinate(0, offsetM));
 426     }
 427  
 428     // return it
 429     return sequence;
 430   }
 431  
 432   /**
 433    * Returns the next array of <code>Coordinate</code>s in the stream.
 434    *
 435    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 436    *      format. The next element returned by the stream should be L_PAREN (the
 437    *      beginning of "(x1 y1, x2 y2, ..., xn yn)") or EMPTY.
 438    *@return                  the next array of <code>Coordinate</code>s in the
 439    *      stream, or an empty array if EMPTY is the next element returned by
 440    *      the stream.
 441    *@throws  IOException     if an I/O error occurs
 442    *@throws  ParseException  if an unexpected token was encountered
 443    *
 444    *@deprecated in favor of functions returning {@link CoordinateSequence}s
 445    */
 446   private Coordinate[] getCoordinates(StreamTokenizer tokenizer) throws IOException, ParseException {
 447     String nextToken = getNextEmptyOrOpener(tokenizer);
 448     if (nextToken.equals(WKTConstants.EMPTY)) {
 449       return new Coordinate[] {};
 450     }
 451     ArrayList coordinates = new ArrayList();
 452     coordinates.add(getPreciseCoordinate(tokenizer));
 453     nextToken = getNextCloserOrComma(tokenizer);
 454     while (nextToken.equals(COMMA)) {
 455       coordinates.add(getPreciseCoordinate(tokenizer));
 456       nextToken = getNextCloserOrComma(tokenizer);
 457     }
 458     Coordinate[] array = new Coordinate[coordinates.size()];
 459     return (Coordinate[]) coordinates.toArray(array);
 460   }
 461  
 462   /**
 463    * Returns the next array of <code>Coordinate</code>s in the stream.
 464    *
 465    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 466    *      format. The next element returned by the stream should be a number.
 467    *@return                  the next array of <code>Coordinate</code>s in the
 468    *      stream.
 469    *@throws  IOException     if an I/O error occurs
 470    *@throws  ParseException  if an unexpected token was encountered
 471    *
 472    *@deprecated in favor of functions returning {@link CoordinateSequence}s
 473    */
 474   private Coordinate[] getCoordinatesNoLeftParen(StreamTokenizer tokenizer) throws IOException, ParseException {
 475     String nextToken = null;
 476     ArrayList coordinates = new ArrayList();
 477     coordinates.add(getPreciseCoordinate(tokenizer));
 478     nextToken = getNextCloserOrComma(tokenizer);
 479     while (nextToken.equals(COMMA)) {
 480       coordinates.add(getPreciseCoordinate(tokenizer));
 481       nextToken = getNextCloserOrComma(tokenizer);
 482     }
 483     Coordinate[] array = new Coordinate[coordinates.size()];
 484     return (Coordinate[]) coordinates.toArray(array);
 485   }
 486  
 487   /**
 488    * Returns the next precise <code>Coordinate</code> in the stream.
 489    *
 490    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 491    *      format. The next element returned by the stream should be a number.
 492    *@return                  the next array of <code>Coordinate</code>s in the
 493    *      stream.
 494    *@throws  IOException     if an I/O error occurs
 495    *@throws  ParseException  if an unexpected token was encountered
 496    *
 497    *@deprecated in favor of functions returning {@link CoordinateSequence}s
 498    */
 499   private Coordinate getPreciseCoordinate(StreamTokenizer tokenizer)
 500       throws IOException, ParseException
 501   {
 502     Coordinate coord = new Coordinate();
 503     coord.x = getNextNumber(tokenizer);
 504     coord.y = getNextNumber(tokenizer);
 505     if (isNumberNext(tokenizer)) {
 506         coord.setZ(getNextNumber(tokenizer));
 507     }
 508     if (isNumberNext(tokenizer)) {
 509       getNextNumber(tokenizer); // ignore M value
 510     }
 511     precisionModel.makePrecise(coord);
 512     return coord;
 513   }
 514  
 515   /**
 516    * Tests if the next token in the stream is a number
 517    *
 518    * @param tokenizer the tokenizer
 519    * @return {@code true} if the next token is a number, otherwise {@code false}
 520    * @throws  IOException     if an I/O error occurs
 521    */
 522   private static boolean isNumberNext(StreamTokenizer tokenizer) throws IOException {
 523     int type = tokenizer.nextToken();
 524     tokenizer.pushBack();
 525     return type == StreamTokenizer.TT_WORD;
 526   }
 527  
 528   /**
 529    * Tests if the next token in the stream is a left opener ({@link #L_PAREN})
 530    *
 531    * @param tokenizer the tokenizer
 532    * @return {@code true} if the next token is a {@link #L_PAREN}, otherwise {@code false}
 533    * @throws  IOException     if an I/O error occurs
 534    */
 535   private static boolean isOpenerNext(StreamTokenizer tokenizer) throws IOException {
 536     int type = tokenizer.nextToken();
 537     tokenizer.pushBack();
 538     return type == '(';
 539   }
 540  
 541   /**
 542    * Parses the next number in the stream.
 543    * Numbers with exponents are handled.
 544    * <tt>NaN</tt> values are handled correctly, and
 545    * the case of the "NaN" symbol is not significant. 
 546    *
 547    * @param  tokenizer        tokenizer over a stream of text in Well-known Text
 548    * @return                  the next number in the stream
 549    * @throws  ParseException  if the next token is not a valid number
 550    * @throws  IOException     if an I/O error occurs
 551    */
 552   private double getNextNumber(StreamTokenizer tokenizer) throws IOException,
 553       ParseException {
 554     int type = tokenizer.nextToken();
 555     switch (type) {
 556       case StreamTokenizer.TT_WORD:
 557       {
 558         if (tokenizer.sval.equalsIgnoreCase(NAN_SYMBOL)) {
 559           return Double.NaN;
 560         }
 561         else {
 562           try {
 563             return Double.parseDouble(tokenizer.sval);
 564           }
 565           catch (NumberFormatException ex) {
 566             throw parseErrorWithLine(tokenizer, "Invalid number: " + tokenizer.sval);
 567           }
 568         }
 569       }
 570     }
 571     throw parseErrorExpected(tokenizer, "number");
 572   }
 573  
 574   /**
 575    *  Returns the next EMPTY or L_PAREN in the stream as uppercase text.
 576    *
 577    *@return                  the next EMPTY or L_PAREN in the stream as uppercase
 578    *      text.
 579    *@throws  ParseException  if the next token is not EMPTY or L_PAREN
 580    *@throws  IOException     if an I/O error occurs
 581    * @param  tokenizer        tokenizer over a stream of text in Well-known Text
 582    */
 583   private static String getNextEmptyOrOpener(StreamTokenizer tokenizer) throws IOException, ParseException {
 584     String nextWord = getNextWord(tokenizer);
 585     if (nextWord.equalsIgnoreCase(WKTConstants.Z)) {
 586       //z = true;
 587       nextWord = getNextWord(tokenizer);
 588     }
 589     else if (nextWord.equalsIgnoreCase(WKTConstants.M)) {
 590       //m = true;
 591       nextWord = getNextWord(tokenizer);
 592     }
 593     else if (nextWord.equalsIgnoreCase(WKTConstants.ZM)) {
 594       //z = true;
 595       //m = true;
 596       nextWord = getNextWord(tokenizer);
 597     }
 598     if (nextWord.equals(WKTConstants.EMPTY) || nextWord.equals(L_PAREN)) {
 599       return nextWord;
 600     }
 601     throw parseErrorExpected(tokenizer, WKTConstants.EMPTY + " or " + L_PAREN);
 602   }
 603  
 604   /**
 605    *  Returns the next ordinate flag information in the stream as uppercase text.
 606    *  This can be Z, M or ZM.
 607    *
 608    *@return                  the next EMPTY or L_PAREN in the stream as uppercase
 609    *      text.
 610    *@throws  ParseException  if the next token is not EMPTY or L_PAREN
 611    *@throws  IOException     if an I/O error occurs
 612    * @param  tokenizer        tokenizer over a stream of text in Well-known Text
 613    */
 614   private static EnumSet<Ordinate> getNextOrdinateFlags(StreamTokenizer tokenizer) throws IOException, ParseException {
 615  
 616     EnumSet<Ordinate> result = EnumSet.of(Ordinate.X, Ordinate.Y);
 617  
 618     String nextWord = lookAheadWord(tokenizer).toUpperCase(Locale.ROOT);
 619     if (nextWord.equalsIgnoreCase(WKTConstants.Z)) {
 620       tokenizer.nextToken();
 621       result.add(Ordinate.Z);
 622     }
 623     else if (nextWord.equalsIgnoreCase(WKTConstants.M)) {
 624       tokenizer.nextToken();
 625       result.add(Ordinate.M);
 626     }
 627     else if (nextWord.equalsIgnoreCase(WKTConstants.ZM)) {
 628       tokenizer.nextToken();
 629       result.add(Ordinate.Z);
 630       result.add(Ordinate.M);
 631     }
 632     return result;
 633   }
 634  
 635   /**
 636    *  Returns the next word in the stream.
 637    *
 638    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 639    *      format. The next token must be a word.
 640    *@return                  the next word in the stream as uppercase text
 641    *@throws  ParseException  if the next token is not a word
 642    *@throws  IOException     if an I/O error occurs
 643    */
 644   private static String lookAheadWord(StreamTokenizer tokenizer) throws IOException, ParseException {
 645     String nextWord = getNextWord(tokenizer);
 646     tokenizer.pushBack();
 647     return nextWord;
 648   }
 649  
 650   /**
 651    *  Returns the next {@link #R_PAREN} or {@link #COMMA} in the stream.
 652    *
 653    *@return                  the next R_PAREN or COMMA in the stream
 654    *@throws  ParseException  if the next token is not R_PAREN or COMMA
 655    *@throws  IOException     if an I/O error occurs
 656    * @param  tokenizer        tokenizer over a stream of text in Well-known Text
 657    */
 658   private static String getNextCloserOrComma(StreamTokenizer tokenizer) throws IOException, ParseException {
 659     String nextWord = getNextWord(tokenizer);
 660     if (nextWord.equals(COMMA) || nextWord.equals(R_PAREN)) {
 661       return nextWord;
 662     }
 663     throw parseErrorExpected(tokenizer, COMMA + " or " + R_PAREN);
 664   }
 665  
 666   /**
 667    *  Returns the next {@link #R_PAREN} in the stream.
 668    *
 669    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 670    *      format. The next token must be R_PAREN.
 671    *@return                  the next R_PAREN in the stream
 672    *@throws  ParseException  if the next token is not R_PAREN
 673    *@throws  IOException     if an I/O error occurs
 674    */
 675   private String getNextCloser(StreamTokenizer tokenizer) throws IOException, ParseException {
 676     String nextWord = getNextWord(tokenizer);
 677     if (nextWord.equals(R_PAREN)) {
 678       return nextWord;
 679     }
 680     throw parseErrorExpected(tokenizer, R_PAREN);
 681   }
 682  
 683   /**
 684    *  Returns the next word in the stream.
 685    *
 686    *@return                  the next word in the stream as uppercase text
 687    *@throws  ParseException  if the next token is not a word
 688    *@throws  IOException     if an I/O error occurs
 689    * @param  tokenizer        tokenizer over a stream of text in Well-known Text
 690    */
 691   private static String getNextWord(StreamTokenizer tokenizer) throws IOException, ParseException {
 692     int type = tokenizer.nextToken();
 693     switch (type) {
 694     case StreamTokenizer.TT_WORD:
 695  
 696       String word = tokenizer.sval;
 697       if (word.equalsIgnoreCase(WKTConstants.EMPTY))
 698           return WKTConstants.EMPTY;
 699       return word;
 700  
 701     case '('return L_PAREN;
 702     case ')'return R_PAREN;
 703     case ','return COMMA;
 704     }
 705     throw parseErrorExpected(tokenizer, "word");
 706   }
 707  
 708   /**
 709    * Creates a formatted ParseException reporting that the current token
 710    * was unexpected.
 711    *
 712    * @param expected a description of what was expected
 713    * @throws AssertionFailedException if an invalid token is encountered
 714    */
 715   private static ParseException parseErrorExpected(StreamTokenizer tokenizer, String expected)
 716   {
 717     // throws Asserts for tokens that should never be seen
 718     if (tokenizer.ttype == StreamTokenizer.TT_NUMBER)
 719       Assert.shouldNeverReachHere("Unexpected NUMBER token");
 720     if (tokenizer.ttype == StreamTokenizer.TT_EOL)
 721       Assert.shouldNeverReachHere("Unexpected EOL token");
 722  
 723     String tokenStr = tokenString(tokenizer);
 724     return parseErrorWithLine(tokenizer, "Expected " + expected + " but found " + tokenStr);
 725   }
 726  
 727   /**
 728    * Creates a formatted ParseException reporting that the current token
 729    * was unexpected.
 730    *
 731    * @param msg a description of what was expected
 732    * @throws AssertionFailedException if an invalid token is encountered
 733    */
 734   private static ParseException parseErrorWithLine(StreamTokenizer tokenizer, String msg)
 735   {
 736     return new ParseException(msg + " (line " + tokenizer.lineno() + ")");
 737   }
 738   
 739   /**
 740    * Gets a description of the current token type
 741    * @param tokenizer the tokenizer
 742    * @return a description of the current token
 743    */
 744   private static String tokenString(StreamTokenizer tokenizer)
 745   {
 746     switch (tokenizer.ttype) {
 747       case StreamTokenizer.TT_NUMBER:
 748         return "<NUMBER>";
 749       case StreamTokenizer.TT_EOL:
 750         return "End-of-Line";
 751       case StreamTokenizer.TT_EOF: return "End-of-Stream";
 752       case StreamTokenizer.TT_WORD: return "'" + tokenizer.sval + "'";
 753     }
 754     return "'" + (char) tokenizer.ttype + "'";
 755   }
 756  
 757   /**
 758    *  Creates a <code>Geometry</code> using the next token in the stream.
 759    *
 760    *@return                  a <code>Geometry</code> specified by the next token
 761    *      in the stream
 762    *@throws  ParseException  if the coordinates used to create a <code>Polygon</code>
 763    *      shell and holes do not form closed linestrings, or if an unexpected
 764    *      token was encountered
 765    *@throws  IOException     if an I/O error occurs
 766    * @param  tokenizer        tokenizer over a stream of text in Well-known Text
 767    */
 768   private Geometry readGeometryTaggedText(StreamTokenizer tokenizer) throws IOException, ParseException {
 769     String type;
 770  
 771     EnumSet<Ordinate> ordinateFlags = EnumSet.of(Ordinate.X, Ordinate.Y);
 772     try {
 773       type = getNextWord(tokenizer).toUpperCase(Locale.ROOT);
 774       if (type.endsWith(WKTConstants.ZM)) {
 775         ordinateFlags.add(Ordinate.Z);
 776         ordinateFlags.add(Ordinate.M);
 777       } else if (type.endsWith(WKTConstants.Z)) {
 778         ordinateFlags.add(Ordinate.Z);
 779       } else if (type.endsWith(WKTConstants.M)) {
 780         ordinateFlags.add(Ordinate.M);
 781       }
 782     } catch (IOException e) {
 783       return null;
 784     } catch (ParseException e) {
 785       return null;
 786     }
 787  
 788     return readGeometryTaggedText(tokenizer, type, ordinateFlags);
 789   }
 790  
 791   private Geometry readGeometryTaggedText(StreamTokenizer tokenizer, String type, EnumSet<Ordinate> ordinateFlags)
 792           throws IOException, ParseException {
 793  
 794     if (ordinateFlags.size() == 2) {
 795       ordinateFlags = getNextOrdinateFlags(tokenizer);
 796     }
 797  
 798     // if we can create a sequence with the required dimension everything is ok, otherwise
 799     // we need to take a different coordinate sequence factory.
 800     // It would be good to not have to try/catch this but if the CoordinateSequenceFactory
 801     // exposed a value indicating which min/max dimension it can handle or even an
 802     // ordinate bit-flag.
 803     try {
 804       csFactory.create(0, toDimension(ordinateFlags), ordinateFlags.contains(Ordinate.M) ? 1 : 0);
 805     } catch (Exception e)
 806     {
 807       geometryFactory = new GeometryFactory(geometryFactory.getPrecisionModel(),
 808               geometryFactory.getSRID(), csFactoryXYZM);
 809     }
 810  
 811     if (type.startsWith(WKTConstants.POINT)) {
 812       return readPointText(tokenizer, ordinateFlags);
 813     }
 814     else if (type.startsWith(WKTConstants.LINESTRING)) {
 815       return readLineStringText(tokenizer, ordinateFlags);
 816     }
 817     else if (type.startsWith(WKTConstants.LINEARRING)) {
 818       return readLinearRingText(tokenizer, ordinateFlags);
 819     }
 820     else if (type.startsWith(WKTConstants.POLYGON)) {
 821       return readPolygonText(tokenizer, ordinateFlags);
 822     }
 823     else if (type.startsWith(WKTConstants.MULTIPOINT)) {
 824       return readMultiPointText(tokenizer, ordinateFlags);
 825     }
 826     else if (type.startsWith(WKTConstants.MULTILINESTRING)) {
 827       return readMultiLineStringText(tokenizer, ordinateFlags);
 828     }
 829     else if (type.startsWith(WKTConstants.MULTIPOLYGON)) {
 830       return readMultiPolygonText(tokenizer, ordinateFlags);
 831     }
 832     else if (type.startsWith(WKTConstants.GEOMETRYCOLLECTION)) {
 833       return readGeometryCollectionText(tokenizer, ordinateFlags);
 834     }
 835     throw parseErrorWithLine(tokenizer, "Unknown geometry type: " + type);
 836   }
 837  
 838   /**
 839    *  Creates a <code>Point</code> using the next token in the stream.
 840    *
 841    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 842    *      format. The next tokens must form a <Point Text>.
 843    *@return                  a <code>Point</code> specified by the next token in
 844    *      the stream
 845    *@throws  IOException     if an I/O error occurs
 846    *@throws  ParseException  if an unexpected token was encountered
 847    */
 848   private Point readPointText(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags) throws IOException, ParseException {
 849     Point point = geometryFactory.createPoint(getCoordinateSequence(tokenizer, ordinateFlags));
 850     return point;
 851   }
 852  
 853   /**
 854    *  Creates a <code>LineString</code> using the next token in the stream.
 855    *
 856    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 857    *      format. The next tokens must form a <LineString Text>.
 858    *@return                  a <code>LineString</code> specified by the next
 859    *      token in the stream
 860    *@throws  IOException     if an I/O error occurs
 861    *@throws  ParseException  if an unexpected token was encountered
 862    */
 863   private LineString readLineStringText(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags) throws IOException, ParseException {
 864     return geometryFactory.createLineString(getCoordinateSequence(tokenizer, ordinateFlags));
 865   }
 866  
 867   /**
 868    *  Creates a <code>LinearRing</code> using the next token in the stream.
 869    *
 870    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 871    *      format. The next tokens must form a <LineString Text>.
 872    *@return                  a <code>LinearRing</code> specified by the next
 873    *      token in the stream
 874    *@throws  IOException     if an I/O error occurs
 875    *@throws  ParseException  if the coordinates used to create the <code>LinearRing</code>
 876    *      do not form a closed linestring, or if an unexpected token was
 877    *      encountered
 878    */
 879   private LinearRing readLinearRingText(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags)
 880     throws IOException, ParseException
 881   {
 882     return geometryFactory.createLinearRing(getCoordinateSequence(tokenizer, ordinateFlags));
 883   }
 884  
 885   /**
 886    *  Creates a <code>MultiPoint</code> using the next tokens in the stream.
 887    *
 888    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 889    *      format. The next tokens must form a <MultiPoint Text>.
 890    *@return                  a <code>MultiPoint</code> specified by the next
 891    *      token in the stream
 892    *@throws  IOException     if an I/O error occurs
 893    *@throws  ParseException  if an unexpected token was encountered
 894    */
 895   private MultiPoint readMultiPointText(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags) throws IOException, ParseException
 896   {
 897     String nextToken = getNextEmptyOrOpener(tokenizer);
 898     if (nextToken.equals(WKTConstants.EMPTY)) {
 899       return geometryFactory.createMultiPoint(new Point[0]);
 900     }
 901     
 902     // check for old-style JTS syntax (no parentheses surrounding Point coordinates) and parse it if present
 903     // MD 2009-02-21 - this is only provided for backwards compatibility for a few versions
 904     if (isAllowOldJtsMultipointSyntax) {
 905       String nextWord = lookAheadWord(tokenizer);
 906       if (nextWord != L_PAREN) {
 907         return geometryFactory.createMultiPoint(
 908             getCoordinateSequenceOldMultiPoint(tokenizer, ordinateFlags));
 909       }
 910     }
 911     
 912     ArrayList points = new ArrayList();
 913     Point point = readPointText(tokenizer, ordinateFlags);
 914     points.add(point);
 915     nextToken = getNextCloserOrComma(tokenizer);
 916     while (nextToken.equals(COMMA)) {
 917       point = readPointText(tokenizer, ordinateFlags);
 918       points.add(point);
 919       nextToken = getNextCloserOrComma(tokenizer);
 920     }
 921     Point[] array = new Point[points.size()];
 922     return geometryFactory.createMultiPoint((Point[]) points.toArray(array));
 923   }
 924  
 925  
 926   /**
 927    *  Creates a <code>Polygon</code> using the next token in the stream.
 928    *
 929    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 930    *      format. The next tokens must form a <Polygon Text>.
 931    *@return                  a <code>Polygon</code> specified by the next token
 932    *      in the stream
 933    *@throws  ParseException  if the coordinates used to create the <code>Polygon</code>
 934    *      shell and holes do not form closed linestrings, or if an unexpected
 935    *      token was encountered.
 936    *@throws  IOException     if an I/O error occurs
 937    */
 938   private Polygon readPolygonText(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags) throws IOException, ParseException {
 939     String nextToken = getNextEmptyOrOpener(tokenizer);
 940     if (nextToken.equals(WKTConstants.EMPTY)) {
 941         return geometryFactory.createPolygon();
 942     }
 943     ArrayList holes = new ArrayList();
 944     LinearRing shell = readLinearRingText(tokenizer, ordinateFlags);
 945     nextToken = getNextCloserOrComma(tokenizer);
 946     while (nextToken.equals(COMMA)) {
 947       LinearRing hole = readLinearRingText(tokenizer, ordinateFlags);
 948       holes.add(hole);
 949       nextToken = getNextCloserOrComma(tokenizer);
 950     }
 951     LinearRing[] array = new LinearRing[holes.size()];
 952     return geometryFactory.createPolygon(shell, (LinearRing[]) holes.toArray(array));
 953   }
 954  
 955   /**
 956    *  Creates a <code>MultiLineString</code> using the next token in the stream.
 957    *
 958    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 959    *      format. The next tokens must form a <MultiLineString Text>.
 960    *@return                  a <code>MultiLineString</code> specified by the
 961    *      next token in the stream
 962    *@throws  IOException     if an I/O error occurs
 963    *@throws  ParseException  if an unexpected token was encountered
 964    */
 965   private MultiLineString readMultiLineStringText(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags)
 966           throws IOException, ParseException {
 967     String nextToken = getNextEmptyOrOpener(tokenizer);
 968     if (nextToken.equals(WKTConstants.EMPTY)) {
 969       return geometryFactory.createMultiLineString();
 970     }
 971  
 972     ArrayList lineStrings = new ArrayList();
 973     do {
 974       LineString lineString = readLineStringText(tokenizer, ordinateFlags);
 975       lineStrings.add(lineString);
 976       nextToken = getNextCloserOrComma(tokenizer);
 977     } while (nextToken.equals(COMMA));
 978  
 979     LineString[] array = new LineString[lineStrings.size()];
 980     return geometryFactory.createMultiLineString((LineString[]) lineStrings.toArray(array));
 981   }
 982  
 983   /**
 984    *  Creates a <code>MultiPolygon</code> using the next token in the stream.
 985    *
 986    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
 987    *      format. The next tokens must form a <MultiPolygon Text>.
 988    *@return                  a <code>MultiPolygon</code> specified by the next
 989    *      token in the stream, or if if the coordinates used to create the
 990    *      <code>Polygon</code> shells and holes do not form closed linestrings.
 991    *@throws  IOException     if an I/O error occurs
 992    *@throws  ParseException  if an unexpected token was encountered
 993    */
 994   private MultiPolygon readMultiPolygonText(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags) throws IOException, ParseException {
 995     String nextToken = getNextEmptyOrOpener(tokenizer);
 996     if (nextToken.equals(WKTConstants.EMPTY)) {
 997       return geometryFactory.createMultiPolygon();
 998     }
 999     ArrayList polygons = new ArrayList();
1000     do {
1001       Polygon polygon = readPolygonText(tokenizer, ordinateFlags);
1002       polygons.add(polygon);
1003       nextToken = getNextCloserOrComma(tokenizer);
1004     } while (nextToken.equals(COMMA));
1005     Polygon[] array = new Polygon[polygons.size()];
1006     return geometryFactory.createMultiPolygon((Polygon[]) polygons.toArray(array));
1007   }
1008  
1009   /**
1010    *  Creates a <code>GeometryCollection</code> using the next token in the
1011    *  stream.
1012    *
1013    *@param  tokenizer        tokenizer over a stream of text in Well-known Text
1014    *      format. The next tokens must form a <GeometryCollection Text>.
1015    *@return                  a <code>GeometryCollection</code> specified by the
1016    *      next token in the stream
1017    *@throws  ParseException  if the coordinates used to create a <code>Polygon</code>
1018    *      shell and holes do not form closed linestrings, or if an unexpected
1019    *      token was encountered
1020    *@throws  IOException     if an I/O error occurs
1021    */
1022   private GeometryCollection readGeometryCollectionText(StreamTokenizer tokenizer, EnumSet<Ordinate> ordinateFlags) throws IOException, ParseException {
1023     String nextToken = getNextEmptyOrOpener(tokenizer);
1024     if (nextToken.equals(WKTConstants.EMPTY)) {
1025       return geometryFactory.createGeometryCollection();
1026     }
1027     ArrayList geometries = new ArrayList();
1028     do {
1029       Geometry geometry = readGeometryTaggedText(tokenizer);
1030       geometries.add(geometry);
1031       nextToken = getNextCloserOrComma(tokenizer);
1032     } while (nextToken.equals(COMMA));
1033  
1034     Geometry[] array = new Geometry[geometries.size()];
1035     return geometryFactory.createGeometryCollection((Geometry[]) geometries.toArray(array));
1036   }
1037  
1038 }
1039  
1040