/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2006 Vivid Solutions * (C) 2001-2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.geometry.iso.io.wkt; import java.io.IOException; import java.io.Reader; import java.io.StreamTokenizer; import java.io.StringReader; import java.util.ArrayList; import java.util.List; import org.geotools.geometry.iso.PositionFactoryImpl; import org.geotools.geometry.iso.coordinate.LineStringImpl; import org.geotools.geometry.iso.coordinate.PointArrayImpl; import org.geotools.geometry.iso.primitive.CurveImpl; import org.geotools.geometry.iso.primitive.PointImpl; import org.geotools.geometry.iso.primitive.RingImpl; import org.geotools.geometry.iso.primitive.SurfaceBoundaryImpl; import org.geotools.geometry.iso.primitive.SurfaceImpl; import org.geotools.geometry.iso.util.AssertionFailedException; import org.opengis.geometry.Geometry; import org.opengis.geometry.PositionFactory; import org.opengis.geometry.coordinate.LineString; import org.opengis.geometry.coordinate.Position; import org.opengis.geometry.primitive.Curve; import org.opengis.geometry.primitive.CurveSegment; import org.opengis.geometry.primitive.OrientableCurve; import org.opengis.geometry.primitive.Point; import org.opengis.geometry.primitive.Ring; import org.opengis.geometry.primitive.Surface; import org.opengis.geometry.primitive.SurfaceBoundary; import org.opengis.referencing.crs.CoordinateReferenceSystem; /** * * @author sanjay * * * @source $URL$ */ public class 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 PrimitiveFactoryImpl primitiveFactory; //private GeometryFactoryImpl geometryFactory; private PositionFactory positionFactory; private CoordinateReferenceSystem crs; private StreamTokenizer tokenizer; public WKTReader(CoordinateReferenceSystem crs){ this( crs, new PositionFactoryImpl(crs) ); } /** * Creates a reader that creates objects using the given * {@link crs}. * @param crs * */ public WKTReader(CoordinateReferenceSystem crs, PositionFactory aPositionFactory ) { this.crs = crs; this.positionFactory = aPositionFactory; } /** * 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. * * @param tokenizer * tokenizer over a stream of text in Well-known Text format. The * next element returned by the stream should be L_PAREN (the * beginning of "(x1 y1, x2 y2, ..., xn yn)") or EMPTY. * @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 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. * * @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: { 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. * * @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() 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. * * @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() 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. * * @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() 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. * * @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() 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) throw new ParseException("Unexpected NUMBER token"); if (tokenizer.ttype == StreamTokenizer.TT_EOL) throw new ParseException("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. * * @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>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 = getNextWord(); if (type.equals(WKTConstants.WKT_POINT)) { return readPointText(); } else if (type.equalsIgnoreCase(WKTConstants.WKT_CURVE)) { return readLineStringText(); } else if ( type.equalsIgnoreCase(WKTConstants.WKT_SURFACE) || type.equalsIgnoreCase(WKTConstants.WKT_POLYGON) ) { return readPolygonText(); } throw new ParseException("Unknown geometry type: " + type); } /** * 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() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return null; } Point point = new PointImpl(positionFactory.createDirectPosition( this.getPreciseCoordinate().getCoordinates() )); //primitiveFactory.createPoint(this.getPreciseCoordinate().getCoordinates()); getNextCloser(); 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() throws IOException, ParseException { return (Curve) this.createCurve(this.getCoordinates()); } /** * Creates a <code>LinearRing</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>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 Ring readLinearRingText() throws IOException, ParseException { List<OrientableCurve> curves = new ArrayList<OrientableCurve>(); curves.add(this.createCurve(this.getCoordinates())); return new RingImpl(curves); //this.primitiveFactory.createRing(curves); } /** * Creates a <code>Polygon</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>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 Surface readPolygonText() throws IOException, ParseException { String nextToken = getNextEmptyOrOpener(); if (nextToken.equals(EMPTY)) { return new SurfaceImpl((SurfaceBoundary) null); //this.primitiveFactory.createSurface((SurfaceBoundary) null); } ArrayList<Ring> holes = new ArrayList<Ring>(); Ring shell = this.readLinearRingText(); nextToken = getNextCloserOrComma(); while (nextToken.equals(COMMA)) { Ring hole = readLinearRingText(); holes.add(hole); nextToken = getNextCloserOrComma(); } SurfaceBoundary sfb = new SurfaceBoundaryImpl(crs, shell, holes); //this.primitiveFactory.createSurfaceBoundary(shell, holes); return new SurfaceImpl(sfb); //this.primitiveFactory.createSurface(sfb); } /** * Creates a curve from a Coordinate array * * @param aCoords * @return */ private OrientableCurve createCurve(Coordinate[] aCoords) { List<Position> points = new ArrayList<Position>(); for (int i = 0; i < aCoords.length; i++) { points.add(this.positionFactory .createPosition(this.positionFactory .createDirectPosition((aCoords[i] .getCoordinates())))); } // Create List of CurveSegment´s (LineString´s) LineString lineString = new LineStringImpl(new PointArrayImpl( points ), 0.0); //this.geometryFactory.createLineString(points); List<CurveSegment> segments = new ArrayList<CurveSegment>(); segments.add(lineString); return new CurveImpl(crs, segments); //this.primitiveFactory.createCurve(segments); } }