/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2002-2008, Open Source Geospatial Foundation (OSGeo) * (C) 2009, Geomatys * * 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. */ package org.geotoolkit.geometry.isoonjts; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Geometry; import org.opengis.geometry.PositionFactory; import org.opengis.geometry.aggregate.AggregateFactory; import org.opengis.geometry.aggregate.MultiPrimitive; import org.opengis.geometry.coordinate.GeometryFactory; import org.opengis.geometry.coordinate.LineString; import org.opengis.geometry.coordinate.Position; import org.opengis.geometry.primitive.Curve; import org.opengis.geometry.primitive.Point; import org.opengis.geometry.primitive.PrimitiveFactory; import org.opengis.geometry.primitive.Ring; import org.opengis.geometry.primitive.SurfaceBoundary; import org.opengis.geometry.primitive.Surface; import java.io.IOException; import java.io.Reader; import java.io.StreamTokenizer; import java.io.StringReader; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import org.opengis.geometry.primitive.CurveSegment; import org.opengis.geometry.primitive.OrientableCurve; /** * This class is used to parse well known text (WKT) which describes an * ISO 19107 Geometry. The grammar described comes from the ISO 19125-1 * spec which describes feature geometry. It doesn't seem to exactly mesh up * with the geometry as described in 19107 so not all of the grammar is supported. * <p/> * * The types in the WKT format, and their mappings: * <ul> * <li> * POINT org.opengis.geometry.primitive.Point * </li> * <li> * LINESTRING org.opengis.geometry.primitive.Curve * </li> * <li> * POLYGON org.opengis.geometry.primitive.Surface * </li> * <li> * MULTIPOINT org.opengis.geometry.coordinate.aggregate.MultiPoint * Note that there is no factory method for MultiPoint. * <br> * For now, to keep implementation-independance I'm returning it as a List * </li> * <li> * MULTILINESTRING no matching type in the Types interfaces * Could also be returned as list. * <br> * Not handled for now * </li> * <li> * MULTIPOLYGON no matching type in the Types interfaces * Could also be returned as list. * <br> * Not handled for now * </li> * </ul>> * Please note that this parser is not thread safe; you can however reuse the parser. * * @author Jody Garnett * @author Joel Skelton * @author Johann Sorel (Geomatys) * @module * @since 2.5 * @version $Id$ */ public class GeometryParser { private static final String EMPTY = "EMPTY"; private static final String COMMA = ","; private static final String L_PAREN = "("; private static final String R_PAREN = ")"; private GeometryFactory geometryFactory; private PrimitiveFactory primitiveFactory; private PositionFactory positionFactory; private AggregateFactory aggregateFactory; /** * Constructor takes pre-created geometry and primitive factories that will be used to * parse the Well Known Text (WKT). The geometries created from the WKT will be created * in the <code>CoordinateReferenceSystem</code> * * @param geometryFactory A <code>GeometryFactory</code> created with a <code>CoordinateReferenceSystem</code> and <code>PrecisionModel</code> * @param primitiveFactory A <code>PrimitiveFactory</code> created with the same crs and precision as above * @param positionFactory A <code>PositionFactory</code> created with the same crs and precision as above * @param aggregateFactory A <Code>AggregateFactory</code> created with the same crs and precision as above */ public GeometryParser(final GeometryFactory geometryFactory, final PrimitiveFactory primitiveFactory, final PositionFactory positionFactory, final AggregateFactory aggregateFactory) { this.geometryFactory = geometryFactory; this.primitiveFactory = primitiveFactory; this.positionFactory = positionFactory; this.aggregateFactory = aggregateFactory; } /** * Provide a GeometryFactory for the parser. * <p> * Should be called prior to use. * @param factory */ public void setFactory(final GeometryFactory factory){ this.geometryFactory = factory; } /** * Provide a PrimitiveFactory for the parser. * <p> * Should be called prior to use. * @param factory */ public void setFactory(final PrimitiveFactory factory){ this.primitiveFactory = factory; } /** * Provide a PositionFactory for the parser. * <p> * Should be called prior to use. * @param factory */ public void setFactory(final PositionFactory factory){ this.positionFactory = factory; } /** * Takes a string containing well known text geometry description and * wraps it in a Reader which is then passed on to parseWKT for handling. * * @param text A string containing the well known text to be parsed. * @return Geometry indicated by text (as created with current factories) */ public Geometry parse(final String text) throws ParseException { return read(new StringReader(text)); } /** * Reads a Well-Known Text representation of a 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(final Reader reader) throws ParseException { StreamTokenizer tokenizer = new StreamTokenizer(reader); setUpTokenizer(tokenizer); try { return readGeometryTaggedText(tokenizer); } catch (IOException e) { throw new ParseException(e.toString(), tokenizer.lineno()); } } /** * Sets up a {@link StreamTokenizer} for use in parsing the geometry text. * * @param tokenizer A <code>StreamTokenizer</code> */ private static void setUpTokenizer(final StreamTokenizer tokenizer) { final int char128 = 128; final int skip32 = 32; final int char255 = 255; // set tokenizer to NOT parse numbers tokenizer.resetSyntax(); tokenizer.wordChars('a', 'z'); tokenizer.wordChars('A', 'Z'); tokenizer.wordChars(char128 + skip32, char255); tokenizer.wordChars('0', '9'); tokenizer.wordChars('-', '-'); tokenizer.wordChars('+', '+'); tokenizer.wordChars('.', '.'); tokenizer.whitespaceChars(0, ' '); tokenizer.commentChar('#'); } /** * Creates a <code>Geometry</code> using the next token in the stream. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next tokens must form a <Geometry Tagged Text>. * @return a <code>Object</code> of the correct type for the next item * 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(final StreamTokenizer tokenizer) throws IOException, ParseException { final String type = getNextWord(tokenizer); if (type.equals("POINT")) { return readPointText(tokenizer); } else if (type.equalsIgnoreCase("LINESTRING")) { return readLineStringText(tokenizer); } else if (type.equalsIgnoreCase("LINEARRING")) { return readLinearRingText(tokenizer); } else if (type.equalsIgnoreCase("POLYGON")) { return readPolygonText(tokenizer); } else if (type.equalsIgnoreCase("MULTIPOINT")) { return readMultiPointText(tokenizer); } else if (type.equalsIgnoreCase("MULTIPOLYGON")) { return readMultiPolygonText(tokenizer); } else if (type.equalsIgnoreCase("GEOMETRYCOLLECTION")) { return readGeometryCollectionText(tokenizer); } else if (type.equalsIgnoreCase("MULTILINESTRING")) { return readMultiLineStringText(tokenizer); } throw new ParseException("Unknown geometry type: " + type, tokenizer.lineno()); } /** * Returns a list of DirectPosition objects which it read from * the StreamTokenizer * * @param tokenizer * @return a <code>List\<DirectPosition\></code> * @throws IOException * @throws ParseException */ private List<Position> getCoordinates(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(tokenizer); List<Position> coordinates = new ArrayList<Position>(); if (!nextToken.equals(EMPTY)) { coordinates.add(getPreciseCoordinate(tokenizer)); nextToken = getNextCloserOrComma(tokenizer); while (nextToken.equals(COMMA)) { coordinates.add(getPreciseCoordinate(tokenizer)); nextToken = getNextCloserOrComma(tokenizer); } } return coordinates; } /** * Parse a single coordinate from a <code>StreamTokenizer</code> * * @param tokenizer * @return a single DirectPosition * @throws IOException * @throws ParseException */ private DirectPosition getPreciseCoordinate(final StreamTokenizer tokenizer) throws IOException, ParseException { double[] coords = new double[2]; coords[0] = getNextNumber(tokenizer); coords[1] = getNextNumber(tokenizer); if (isNumberNext(tokenizer)) { coords[1] = getNextNumber(tokenizer); } return positionFactory.createDirectPosition(coords); } private boolean isNumberNext(final StreamTokenizer tokenizer) 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. * * @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(final StreamTokenizer tokenizer) throws IOException, ParseException { int type = tokenizer.nextToken(); switch (type) { case StreamTokenizer.TT_WORD: { try { return Double.parseDouble(tokenizer.sval); } catch (NumberFormatException ex) { throw new ParseException("Invalid number: " + tokenizer.sval, tokenizer.lineno()); } } default: } parseError("number", tokenizer); return 0.0; } /** * Returns the next EMPTY or L_PAREN in the stream as uppercase text. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next token must be EMPTY or L_PAREN. * @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(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextWord = getNextWord(tokenizer); if (nextWord.equals(EMPTY) || nextWord.equals(L_PAREN)) { return nextWord; } parseError(EMPTY + " or " + L_PAREN, tokenizer); return null; } /** * Returns the next R_PAREN or COMMA in the stream. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next token must be R_PAREN or COMMA. * @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(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextWord = getNextWord(tokenizer); if (nextWord.equals(COMMA) || nextWord.equals(R_PAREN)) { return nextWord; } parseError(COMMA + " or " + R_PAREN, tokenizer); return null; } /** * Returns the next R_PAREN in the stream. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next token must be R_PAREN. * @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(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextWord = getNextWord(tokenizer); if (nextWord.equals(R_PAREN)) { return nextWord; } parseError(R_PAREN, tokenizer); return null; } /** * Returns the next word in the stream. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next token must be a word. * @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(final StreamTokenizer tokenizer) throws IOException, ParseException { int type = tokenizer.nextToken(); String value; switch (type) { case StreamTokenizer.TT_WORD: String word = tokenizer.sval; if (word.equalsIgnoreCase(EMPTY)) { value = EMPTY; } value = word; break; case'(': value = L_PAREN; break; case')': value = R_PAREN; break; case',': value = COMMA; break; default: parseError("word", tokenizer); value = null; break; } return value; } /** * Throws a formatted ParseException for the current token. * * @param expected a description of what was expected * @throws ParseException */ private void parseError(final String expected, final StreamTokenizer tokenizer) throws ParseException { String tokenStr = tokenString(tokenizer); throw new ParseException("Expected " + expected + " but found " + tokenStr, 0); } /** * Gets a description of the current token * * @return a description of the current token */ private String tokenString(final StreamTokenizer tokenizer) { 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 + "'"; default: } return "'" + (char) tokenizer.ttype + "'"; } /** * Creates a <code>Point</code> using the next token in the stream. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next tokens must form a <Point Text>. * @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(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(tokenizer); if (nextToken.equals(EMPTY)) { return primitiveFactory.createPoint(new double[2]); } Point point = primitiveFactory.createPoint(getPreciseCoordinate(tokenizer)); getNextCloser(tokenizer); return point; } /** * Creates a <code>LineString</code> using the next token in the stream. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next tokens must form a <LineString Text>. * @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 Curve readLineStringText(final StreamTokenizer tokenizer) throws IOException, ParseException { final List<Position> coordList = getCoordinates(tokenizer); final LineString lineString = geometryFactory.createLineString(coordList); final List<CurveSegment> curveSegmentList = Collections.singletonList((CurveSegment)lineString); return primitiveFactory.createCurve(curveSegmentList); } /** * Creates a <code>Curve</code> using the next token in the stream. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next tokens must form a <LineString Text>. * @return a <code>Curve</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>Curve</code> * do not form a closed linestring, or if an unexpected token was * encountered */ private Curve readLinearRingText(final StreamTokenizer tokenizer) throws IOException, ParseException { List<Position> coordList = getCoordinates(tokenizer); LineString lineString = geometryFactory.createLineString(coordList); List<CurveSegment> curveSegmentList = Collections.singletonList((CurveSegment)lineString); return primitiveFactory.createCurve(curveSegmentList); } /** * 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 List toPoints(final List coordinates) { List<Position> points = new ArrayList<Position>(); for (int i=0,n=coordinates.size(); i<n; i++) { points.add(positionFactory.createPosition((Point)coordinates.get(i))); } return points; } /** * Creates a <code>SurfaceBoundary</code> using the next token in the stream. * * @param tokenizer tokenizer over a stream of text in Well-known Text * format. The next tokens must form a <Polygon Text>. * @return a <code>Surface</code> specified by the vertices 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 Surface readPolygonText(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(tokenizer); if (nextToken.equals(EMPTY)) { return null; } Curve curve = readLinearRingText(tokenizer); List<OrientableCurve> curveList = Collections.singletonList((OrientableCurve)curve); Ring shell = primitiveFactory.createRing(curveList); //Ring shell = readLinearRingText(tokenizer); List<Ring> holes = new ArrayList<Ring>(); nextToken = getNextCloserOrComma(tokenizer); while (nextToken.equals(COMMA)) { Curve holecurve = readLinearRingText(tokenizer); List<OrientableCurve> holeList = Collections.singletonList((OrientableCurve)holecurve); Ring hole = primitiveFactory.createRing(holeList); //Ring hole = readLinearRingText(tokenizer); holes.add(hole); nextToken = getNextCloserOrComma(tokenizer); } SurfaceBoundary sb = primitiveFactory.createSurfaceBoundary(shell, holes); return primitiveFactory.createSurface(sb); } /** * Creates a {@code MultiPrimitive} using the next token in the stream. * * @param tokenizer tokenizer on top of a stream of text in Well-known Text * format. The next tokens must form a <Polygon Text>. * @return a <code>MultiPrimitive</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 MultiPrimitive readMultiPolygonText(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(tokenizer); if (nextToken.equals(EMPTY)) { return null; } MultiPrimitive multi = geometryFactory.createMultiPrimitive(); Surface surface = readPolygonText(tokenizer); //multi.getElements().add(surface); Set elements = multi.getElements(); elements.add(surface); nextToken = getNextCloserOrComma(tokenizer); while (nextToken.equals(COMMA)) { surface = readPolygonText(tokenizer); //multi.getElements().add(surface); elements.add(surface); nextToken = getNextCloserOrComma(tokenizer); } return multi; } /** * Creates a {@code MultiPrimitive} using the next token in the stream. * * @param tokenizer tokenizer on top of a stream of text in Well-known Text * format. The next tokens must form a <Point Text>. * @return a <code>MultiPrimitive</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 MultiPrimitive readMultiPointText(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(tokenizer); if (nextToken.equals(EMPTY)) { return null; } MultiPrimitive multi = geometryFactory.createMultiPrimitive(); Point point = primitiveFactory.createPoint(getPreciseCoordinate(tokenizer)); //multi.getElements().add(point); Set elements = multi.getElements(); elements.add(point); nextToken = getNextCloserOrComma(tokenizer); while (nextToken.equals(COMMA)) { point = primitiveFactory.createPoint(getPreciseCoordinate(tokenizer)); //multi.getElements().add(point); elements.add(point); nextToken = getNextCloserOrComma(tokenizer); } return multi; } /** * Creates a {@code MultiPrimitive} out of a GEOMETRYCOLLECCTION specifier. * * @param tokenizer tokenizer on top of a stream of text in Well-known Text * format. * @return a <code>MultiPrimitive</code> specified by the next tokens * in the stream * @throws ParseException * @throws IOException if an I/O error occurs */ private MultiPrimitive readGeometryCollectionText(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(tokenizer); if (nextToken.equals(EMPTY)) { return null; } MultiPrimitive multi = geometryFactory.createMultiPrimitive(); Geometry geom = readGeometryTaggedText(tokenizer); //multi.getElements().add(geom); Set elements = multi.getElements(); elements.add(geom); nextToken = getNextCloserOrComma(tokenizer); while (nextToken.equals(COMMA)) { geom = readGeometryTaggedText(tokenizer); //multi.getElements().add(geom); elements.add(geom); nextToken = getNextCloserOrComma(tokenizer); } return multi; } /** * Creates a {@code MultiPrimitive} out of a MULTILINESTRING specifier * * @param tokenizer tokenizer on top of a stream of text in Well-known Text * format. * @return a <code>MultiPrimitive</code> specified by the next tokens * in the stream * @throws ParseException * @throws IOException if an I/O error occurs */ private MultiPrimitive readMultiLineStringText(final StreamTokenizer tokenizer) throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(tokenizer); if (nextToken.equals(EMPTY)) { return null; } MultiPrimitive multi = geometryFactory.createMultiPrimitive(); Curve curve = readLineStringText(tokenizer); //multi.getElements().add(curve); Set elements = multi.getElements(); elements.add(curve); nextToken = getNextCloserOrComma(tokenizer); while (nextToken.equals(COMMA)) { curve = readLineStringText(tokenizer); //multi.getElements().add(curve); elements.add(curve); nextToken = getNextCloserOrComma(tokenizer); } return multi; } }