/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2009 - 2016, 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 WKTReader to handle SQL MM types such as Curve. * We have subclassed so that our implementation can be used anywhere * a WKTReader is needed. We would of tried for more code reuse except * the base class has reduced everything to private methods. * * This class also contains code written by Mark Leslie for PostGIS while working * at Refractions Research with whom we have a code contribution agreement. */ package org.geotools.geometry.jts; import java.io.IOException; import java.io.Reader; import java.io.StreamTokenizer; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; 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.geom.impl.CoordinateArraySequence; import com.vividsolutions.jts.io.ParseException; import com.vividsolutions.jts.io.WKTReader; import com.vividsolutions.jts.util.Assert; import com.vividsolutions.jts.util.AssertionFailedException; /** * Create a geometry from SQL Multi-Media Extension Well-Known Text which allows curves. * * @source $URL$ * @version 1.7 * @see WKTWriter2 */ public class WKTReader2 extends WKTReader { private static final String EMPTY = "EMPTY"; private static final String COMMA = ","; private static final String L_PAREN = "("; private static final String R_PAREN = ")"; private static final String NAN_SYMBOL = "NaN"; private CurvedGeometryFactory geometryFactory; private PrecisionModel precisionModel; private StreamTokenizer tokenizer; /** * Creates a reader that creates objects using the default {@link GeometryFactory}. */ public WKTReader2() { this(JTSFactoryFinder.getGeometryFactory( null )); } /** * Creates a reader that creates objects using the default {@link GeometryFactory}. */ public WKTReader2(double tolerance) { this(new CurvedGeometryFactory(JTSFactoryFinder.getGeometryFactory(null), tolerance)); } /** * Creates a reader that creates objects using the given {@link GeometryFactory}. * *@param geometryFactory * the factory used to create <code>Geometry</code>s. */ public WKTReader2(GeometryFactory geometryFactory) { if (geometryFactory instanceof CurvedGeometryFactory) { this.geometryFactory = (CurvedGeometryFactory) geometryFactory; } else { this.geometryFactory = new CurvedGeometryFactory(geometryFactory, Double.MAX_VALUE); } precisionModel = geometryFactory.getPrecisionModel(); } /** * Reads a Well-Known Text representation of a {@link Geometry} from a {@link String}. * * @param wellKnownText * one or more <Geometry Tagged Text>strings (see the OpenGIS Simple Features * Specification) separated by whitespace * @return a <code>Geometry</code> specified by <code>wellKnownText</code> * @throws ParseException * if a parsing problem occurs */ public Geometry read(String wellKnownText) throws ParseException { StringReader reader = new StringReader(wellKnownText); try { return read(reader); } finally { reader.close(); } } /** * Reads a Well-Known Text representation of a {@link Geometry} from a {@link Reader}. * *@param reader * a Reader which will return a <Geometry Tagged Text> string (see the OpenGIS Simple * Features Specification) *@return a <code>Geometry</code> read from <code>reader</code> *@throws ParseException * if a parsing problem occurs */ public Geometry read(Reader reader) throws ParseException { tokenizer = new StreamTokenizer(reader); // set tokenizer to NOT parse numbers tokenizer.resetSyntax(); tokenizer.wordChars('a', 'z'); tokenizer.wordChars('A', 'Z'); tokenizer.wordChars(128 + 32, 255); tokenizer.wordChars('0', '9'); tokenizer.wordChars('-', '-'); tokenizer.wordChars('+', '+'); tokenizer.wordChars('.', '.'); tokenizer.whitespaceChars(0, ' '); tokenizer.commentChar('#'); try { return readGeometryTaggedText(); } catch (IOException e) { throw new ParseException(e.toString()); } } /** * Returns the next array of <code>Coordinate</code>s in the stream. * *@return the next array of <code>Coordinate</code>s in the stream, or an empty array if EMPTY * is the next element returned by the stream. *@throws IOException * if an I/O error occurs *@throws ParseException * if an unexpected token was encountered */ private Coordinate[] getCoordinates() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return new Coordinate[] {}; } ArrayList coordinates = new ArrayList(); coordinates.add(getPreciseCoordinate()); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { coordinates.add(getPreciseCoordinate()); nextToken = getNextCloserOrComma(); } Coordinate[] array = new Coordinate[coordinates.size()]; return (Coordinate[]) coordinates.toArray(array); } private List<Coordinate> getCoordinateList(boolean openExpected) throws IOException, ParseException { String nextToken; if( openExpected ){ nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return Collections.emptyList(); } } ArrayList<Coordinate> coordinates = new ArrayList<Coordinate>(); coordinates.add(getPreciseCoordinate()); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { coordinates.add(getPreciseCoordinate()); nextToken = getNextCloserOrComma(); } Coordinate[] array = new Coordinate[coordinates.size()]; return coordinates; } private Coordinate getPreciseCoordinate() throws IOException, ParseException { Coordinate coord = new Coordinate(); coord.x = getNextNumber(); coord.y = getNextNumber(); if (isNumberNext()) { coord.z = getNextNumber(); } precisionModel.makePrecise(coord); return coord; } private boolean isNumberNext() throws IOException { int type = tokenizer.nextToken(); tokenizer.pushBack(); return type == StreamTokenizer.TT_WORD; } /** * Parses the next number in the stream. Numbers with exponents are handled. <tt>NaN</tt> values * are handled correctly, and the case of the "NaN" token is not significant. * *@param tokenizer * tokenizer over a stream of text in Well-known Text format. The next token must be * a number. *@return the next number in the stream *@throws ParseException * if the next token is not a valid number *@throws IOException * if an I/O error occurs */ private double getNextNumber() throws IOException, ParseException { int type = tokenizer.nextToken(); switch (type) { case StreamTokenizer.TT_WORD: { if (tokenizer.sval.equalsIgnoreCase(NAN_SYMBOL)) { return Double.NaN; } else { try { return Double.parseDouble(tokenizer.sval); } catch (NumberFormatException ex) { throw new ParseException("Invalid number: " + tokenizer.sval); } } } } parseError("number"); return 0.0; } /** * Returns the next EMPTY or L_PAREN in the stream as uppercase text. * *@return the next EMPTY or L_PAREN in the stream as uppercase text. *@throws ParseException * if the next token is not EMPTY or L_PAREN *@throws IOException * if an I/O error occurs */ private String getNextEmptyOrOpener() throws IOException, ParseException { String nextWord = getNextWord(); if (nextWord.equals(EMPTY) || nextWord.equals(L_PAREN)) { return nextWord; } parseError(EMPTY + " or " + L_PAREN); return null; } /** * Returns the next R_PAREN or COMMA in the stream. * *@return the next R_PAREN or COMMA in the stream *@throws ParseException * if the next token is not R_PAREN or COMMA *@throws IOException * if an I/O error occurs */ private String getNextCloserOrComma() throws IOException, ParseException { String nextWord = getNextWord(); if (nextWord.equals(COMMA) || nextWord.equals(R_PAREN)) { return nextWord; } parseError(COMMA + " or " + R_PAREN); return null; } /** * Returns the next R_PAREN in the stream. * *@return the next R_PAREN in the stream *@throws ParseException * if the next token is not R_PAREN *@throws IOException * if an I/O error occurs */ private String getNextCloser() throws IOException, ParseException { String nextWord = getNextWord(); if (nextWord.equals(R_PAREN)) { return nextWord; } parseError(R_PAREN); return null; } /** * Returns the next word in the stream. * *@return the next word in the stream as uppercase text *@throws ParseException * if the next token is not a word *@throws IOException * if an I/O error occurs */ private String getNextWord() throws IOException, ParseException { int type = tokenizer.nextToken(); switch (type) { case StreamTokenizer.TT_WORD: String word = tokenizer.sval; if (word.equalsIgnoreCase(EMPTY)) return EMPTY; return word; case '(': return L_PAREN; case ')': return R_PAREN; case ',': return COMMA; } parseError("word"); return null; } /** * Throws a formatted ParseException for the current token. * * @param expected * a description of what was expected * @throws ParseException * @throws AssertionFailedException * if an invalid token is encountered */ private void parseError(String expected) throws ParseException { // throws Asserts for tokens that should never be seen if (tokenizer.ttype == StreamTokenizer.TT_NUMBER) Assert.shouldNeverReachHere("Unexpected NUMBER token"); if (tokenizer.ttype == StreamTokenizer.TT_EOL) Assert.shouldNeverReachHere("Unexpected EOL token"); String tokenStr = tokenString(); throw new ParseException("Expected " + expected + " but found " + tokenStr); } /** * Gets a description of the current token * * @return a description of the current token */ private String tokenString() { switch (tokenizer.ttype) { case StreamTokenizer.TT_NUMBER: return "<NUMBER>"; case StreamTokenizer.TT_EOL: return "End-of-Line"; case StreamTokenizer.TT_EOF: return "End-of-Stream"; case StreamTokenizer.TT_WORD: return "'" + tokenizer.sval + "'"; } return "'" + (char) tokenizer.ttype + "'"; } /** * Creates a <code>Geometry</code> using the next token in the stream. * *@return a <code>Geometry</code> specified by the next token in the stream *@throws ParseException * if the coordinates used to create a <code>Polygon</code> shell and holes do not * form closed linestrings, or if an unexpected token was encountered *@throws IOException * if an I/O error occurs */ private Geometry readGeometryTaggedText() throws IOException, ParseException { String type = null; try { type = getNextWord(); } catch (IOException e) { return null; } catch (ParseException e) { return null; } if (type.equalsIgnoreCase("POINT")) { return readPointText(); } else if (type.equalsIgnoreCase("LINESTRING")) { return readLineStringText(); } else if (type.equalsIgnoreCase("LINEARRING")) { return readLinearRingText(); } else if (type.equalsIgnoreCase("POLYGON")) { return readPolygonText(); } else if (type.equalsIgnoreCase("MULTIPOINT")) { return readMultiPointText(); } else if (type.equalsIgnoreCase("MULTILINESTRING")) { return readMultiLineStringText(); } else if (type.equalsIgnoreCase("MULTICURVE")) { return readMultiCurveText(); } else if (type.equalsIgnoreCase("MULTIPOLYGON")) { return readMultiPolygonText(); } else if (type.equalsIgnoreCase("GEOMETRYCOLLECTION")) { return readGeometryCollectionText(); } else if (type.equalsIgnoreCase("CIRCULARSTRING")) { return readCircularStringText(); } else if (type.equalsIgnoreCase("COMPOUNDCURVE")) { return readCompoundCurveText(); } else if (type.equalsIgnoreCase("CURVEPOLYGON")) { return readCurvePolygonText(); } else if (type.equalsIgnoreCase("MULTISURFACE")) { return readMultiSurfaceText(); } throw new ParseException("Unknown geometry type: " + type); } /** * Creates a <code>Point</code> using the next token in the stream. * *@return a <code>Point</code> specified by the next token in the stream *@throws IOException * if an I/O error occurs *@throws ParseException * if an unexpected token was encountered */ private Point readPointText() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return geometryFactory.createPoint((Coordinate) null); } Point point = geometryFactory.createPoint(getPreciseCoordinate()); getNextCloser(); return point; } /** * Creates a <code>LineString</code> using the next token in the stream. * *@return a <code>LineString</code> specified by the next token in the stream *@throws IOException * if an I/O error occurs *@throws ParseException * if an unexpected token was encountered */ private LineString readLineStringText() throws IOException, ParseException { return geometryFactory.createLineString(getCoordinates()); } /** * Creates a <code>LineString</code> using the next token in the stream. * * @return * @throws IOException * @throws ParseException */ private LineString readCircularStringText() throws IOException, ParseException { List<Coordinate> coordinates = getCoordinateList( true ); if (coordinates.size() == 0) { return geometryFactory.createCurvedGeometry(new LiteCoordinateSequence( new Coordinate[0])); } else if (coordinates.size() < 3) { throw new ParseException("A CIRCULARSTRING must contain at least 3 control points"); } else { double[] controlPoints = toControlPoints(coordinates); return geometryFactory.createCurvedGeometry(2, controlPoints); } } private double[] toControlPoints(List<Coordinate> coordinates) { double[] result = new double[coordinates.size() * 2]; for (int i = 0; i < coordinates.size(); i++) { Coordinate c = coordinates.get(i); result[i * 2] = c.x; result[i * 2 + 1] = c.y; } return result; } private LineString readCompoundCurveText() throws IOException, ParseException { List<LineString> lineStrings = getLineStrings(); return geometryFactory.createCurvedGeometry(lineStrings); } /** * Handles mixed line string notation - either LineString (the default) or CircularCurve. * Isolated as a seperate method as I think we will need to call this from the polygon code. * * @return List of LineString (defined in a mixed format) * @throws IOException * @throws ParseException */ List<LineString> getLineStrings() throws IOException, ParseException { ArrayList<LineString> lineStrings = new ArrayList<LineString>(); String nextWord = getNextEmptyOrOpener(); if (nextWord.equals(EMPTY)) { return lineStrings; } // must be an opener! nextWord = COMMA; while( nextWord.equals( COMMA )){ nextWord = getNextWord(); if( nextWord.equals(L_PAREN) ){ List<Coordinate> coords = getCoordinateList(false); LineString lineString = geometryFactory.createLineString( coords.toArray( new Coordinate[coords.size()])); lineStrings.add(lineString); } else if( nextWord.equalsIgnoreCase("CIRCULARSTRING")){ LineString circularString = readCircularStringText(); lineStrings.add(circularString); } else if( nextWord.equalsIgnoreCase("COMPOUNDCURVE")){ LineString compound = readCompoundCurveText(); lineStrings.add(compound); } nextWord = getNextCloserOrComma(); } return lineStrings; } /** * This method will read a LineString, CircularString or CompoundCurve and return the result as a LinearRing. * @return LinearRing * <p> * This method expects either "EMPTY", "(", "CIRCULARSTRING", or "COMPOIUNDCURVE" to start out with. * * @throws IOException * @throws ParseException */ private LinearRing readCurvedLinearRingText() throws IOException, ParseException { String nextWord = getNextWord(); if( nextWord.equals(L_PAREN) ){ List<Coordinate> coords = getCoordinateList(false); return new LinearRing(new CoordinateArraySequence( coords.toArray(new Coordinate[coords.size()])), geometryFactory); } else if( nextWord.equalsIgnoreCase("CIRCULARSTRING")){ return (LinearRing) readCircularStringText(); } else if( nextWord.equalsIgnoreCase("COMPOUNDCURVE")){ return (LinearRing) readCompoundCurveText(); } else { parseError(L_PAREN + ", CIRCULARSTRING or COMPOUNDCURVE"); return null; } } /** * Creates a <code>LinearRing</code> using the next token in the stream. * * @return a <code>LinearRing</code> specified by the next token in the stream * @throws IOException if an I/O error occurs * @throws ParseException if the coordinates used to create the <code>LinearRing</code> do not * form a closed linestring, or if an unexpected token was encountered */ private LinearRing readLinearRingText() throws IOException, ParseException { return geometryFactory.createLinearRing(getCoordinates()); } /** * Creates a <code>MultiPoint</code> using the next token in the stream. * * @return a <code>MultiPoint</code> specified by the next token in the stream * @throws IOException if an I/O error occurs * @throws ParseException if an unexpected token was encountered */ private MultiPoint readMultiPointText() throws IOException, ParseException { return geometryFactory.createMultiPoint(toPoints(getCoordinatesForMultiPoint())); } /** * Get a Coordinate array for a MultiPoint. Specifically handle both WKT styles: * MULTIPOINT (111 -47, 110 -46.5) and MULTIPOINT ((111 -47), (110 -46.5)). * @return An Array of Coordinates * @throws IOException if an I/O error occurs * @throws ParseException if an unexpected token was encountered */ private Coordinate[] getCoordinatesForMultiPoint() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return new Coordinate[] {}; } // Check for inner parens boolean innerParens = false; try { String peek = getNextWord(); innerParens = peek.equals(L_PAREN); } catch(ParseException ex) { // Do nothing } finally { tokenizer.pushBack(); } if (innerParens) { ArrayList coordinates = new ArrayList(); Coordinate[] coords = getCoordinates(); coordinates.add(coords[0]); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { coords = getCoordinates(); coordinates.add(coords[0]); nextToken = getNextCloserOrComma(); } Coordinate[] array = new Coordinate[coordinates.size()]; return (Coordinate[]) coordinates.toArray(array); } else { ArrayList coordinates = new ArrayList(); coordinates.add(getPreciseCoordinate()); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { coordinates.add(getPreciseCoordinate()); nextToken = getNextCloserOrComma(); } Coordinate[] array = new Coordinate[coordinates.size()]; return (Coordinate[]) coordinates.toArray(array); } } /** * Creates an array of <code>Point</code>s having the given <code>Coordinate</code> s. * *@param coordinates * the <code>Coordinate</code>s with which to create the <code>Point</code>s *@return <code>Point</code>s created using this <code>WKTReader</code> s * <code>GeometryFactory</code> */ private Point[] toPoints(Coordinate[] coordinates) { ArrayList points = new ArrayList(); for (int i = 0; i < coordinates.length; i++) { points.add(geometryFactory.createPoint(coordinates[i])); } return (Point[]) points.toArray(new Point[] {}); } /** * Creates a <code>Polygon</code> using the next token in the stream. * *@return a <code>Polygon</code> specified by the next token in the stream *@throws ParseException * if the coordinates used to create the <code>Polygon</code> shell and holes do not * form closed linestrings, or if an unexpected token was encountered. *@throws IOException * if an I/O error occurs */ private Polygon readPolygonText() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return geometryFactory.createPolygon(geometryFactory .createLinearRing(new Coordinate[] {}), new LinearRing[] {}); } ArrayList holes = new ArrayList(); LinearRing shell = readLinearRingText(); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { LinearRing hole = readLinearRingText(); holes.add(hole); nextToken = getNextCloserOrComma(); } LinearRing[] array = new LinearRing[holes.size()]; return geometryFactory.createPolygon(shell, (LinearRing[]) holes.toArray(array)); } private MultiLineString readMultiCurveText() throws IOException, ParseException { List<LineString> lineStrings = getLineStrings(); return geometryFactory.createMultiCurve(lineStrings); } private Polygon readCurvePolygonText() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return geometryFactory.createCurvePolygon( geometryFactory .createLinearRing(new Coordinate[] {}), new LinearRing[] {}); } if( !nextToken.equals( L_PAREN )){ parseError("Ring expected"); } LinearRing shell = readCurvedLinearRingText(); ArrayList holes = new ArrayList(); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { LinearRing hole = readCurvedLinearRingText(); holes.add(hole); nextToken = getNextCloserOrComma(); } LinearRing[] array = new LinearRing[holes.size()]; return geometryFactory.createCurvePolygon(shell, (LinearRing[]) holes.toArray(array)); } /** * Creates a <code>MultiLineString</code> using the next token in the stream. * *@return a <code>MultiLineString</code> specified by the next token in the stream *@throws IOException * if an I/O error occurs *@throws ParseException * if an unexpected token was encountered */ private com.vividsolutions.jts.geom.MultiLineString readMultiLineStringText() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return geometryFactory.createMultiLineString(new LineString[] {}); } ArrayList lineStrings = new ArrayList(); LineString lineString = readLineStringText(); lineStrings.add(lineString); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { lineString = readLineStringText(); lineStrings.add(lineString); nextToken = getNextCloserOrComma(); } LineString[] array = new LineString[lineStrings.size()]; return geometryFactory.createMultiLineString((LineString[]) lineStrings.toArray(array)); } /** * Creates a <code>MultiPolygon</code> using the next token in the stream. * *@return a <code>MultiPolygon</code> specified by the next token in the stream, or if if the * coordinates used to create the <code>Polygon</code> shells and holes do not form * closed linestrings. *@throws IOException * if an I/O error occurs *@throws ParseException * if an unexpected token was encountered */ private MultiPolygon readMultiPolygonText() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return geometryFactory.createMultiPolygon(new Polygon[] {}); } ArrayList polygons = new ArrayList(); Polygon polygon = readPolygonText(); polygons.add(polygon); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { polygon = readPolygonText(); polygons.add(polygon); nextToken = getNextCloserOrComma(); } Polygon[] array = new Polygon[polygons.size()]; return geometryFactory.createMultiPolygon((Polygon[]) polygons.toArray(array)); } /** * Creates a <code>MultiSurface</code> using the next token in the stream. * * @return a <code>MultiSurface</code> specified by the next token in the stream, or if if the * coordinates used to create the <code>Polygon</code> shells and holes do not form * closed linestrings. * @throws IOException if an I/O error occurs * @throws ParseException if an unexpected token was encountered */ private MultiPolygon readMultiSurfaceText() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return geometryFactory.createMultiSurface(new ArrayList<Polygon>()); } ArrayList polygons = new ArrayList(); // must be an opener! String nextWord = COMMA; while( nextWord.equals( COMMA )){ nextWord = getNextWord(); if( nextWord.equals(L_PAREN) || nextWord.equals(EMPTY) ){ tokenizer.pushBack(); Polygon polygon = readPolygonText(); polygons.add(polygon); } else if (nextWord.equalsIgnoreCase("CURVEPOLYGON")) { Polygon polygon = readCurvePolygonText(); polygons.add(polygon); } nextWord = getNextCloserOrComma(); } return geometryFactory.createMultiSurface(polygons); } /** * Creates a <code>GeometryCollection</code> using the next token in the stream. * * @return a <code>GeometryCollection</code> specified by the next token in the stream * @throws ParseException * if the coordinates used to create a <code>Polygon</code> shell and holes do not * form closed linestrings, or if an unexpected token was encountered * @throws IOException * if an I/O error occurs */ private GeometryCollection readGeometryCollectionText() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return geometryFactory.createGeometryCollection(new Geometry[] {}); } ArrayList geometries = new ArrayList(); Geometry geometry = readGeometryTaggedText(); geometries.add(geometry); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { geometry = readGeometryTaggedText(); geometries.add(geometry); nextToken = getNextCloserOrComma(); } Geometry[] array = new Geometry[geometries.size()]; return geometryFactory.createGeometryCollection((Geometry[]) geometries.toArray(array)); } }