/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-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.text; import org.geotools.geometry.GeometryBuilder; 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.complex.ComplexFactory; import org.opengis.geometry.coordinate.GeometryFactory; import org.opengis.geometry.coordinate.LineString; import org.opengis.geometry.primitive.Curve; import org.opengis.geometry.primitive.Point; import org.opengis.geometry.primitive.Primitive; 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; /** * 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. *
* * The types in the WKT format, and their mappings: *CoordinateReferenceSystem
*
* @param geometryFactory A GeometryFactory
created with a CoordinateReferenceSystem
and PrecisionModel
* @param primitiveFactory A PrimitiveFactory
created with the same crs and precision as above
* @param positionFactory A PositionFactory
created with the same crs and precision as above
* @param aggregateFactory A AggregateFactory
created with the same crs and precision as above
*/
public WKTParser(GeometryFactory geometryFactory, PrimitiveFactory primitiveFactory, PositionFactory positionFactory, AggregateFactory aggregateFactory) {
this.geometryFactory = geometryFactory;
this.primitiveFactory = primitiveFactory;
this.positionFactory = positionFactory;
this.aggregateFactory = aggregateFactory;
}
/**
* Provide a GeometryFactory for the parser.
* * Should be called prior to use. * @param factory */ public void setFactory( GeometryFactory factory){ this.geometryFactory = factory; } /** * Provide a PrimitiveFactory for the parser. *
* Should be called prior to use. * @param factory */ public void setFactory( PrimitiveFactory factory){ this.primitiveFactory = factory; } /** * Provide a PositionFactory for the parser. *
* Should be called prior to use.
* @param factory
*/
public void setFactory( 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(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 Geometry
read from reader
* @throws ParseException if a parsing problem occurs
*/
public Geometry read(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 StreamTokenizer
*/
private void setUpTokenizer(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 Geometry
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 Object
of the correct type for the next item
* in the stream
* @throws ParseException if the coordinates used to create a Polygon
* 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(StreamTokenizer tokenizer) throws IOException, ParseException {
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 List\
* @throws IOException
* @throws ParseException
*/
private List getCoordinates(StreamTokenizer tokenizer)
throws IOException, ParseException {
String nextToken = getNextEmptyOrOpener(tokenizer);
List coordinates = new ArrayList();
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 StreamTokenizer
*
* @param tokenizer
* @return a single DirectPosition
* @throws IOException
* @throws ParseException
*/
private DirectPosition getPreciseCoordinate(StreamTokenizer tokenizer)
throws IOException, ParseException {
DirectPosition pos = geometryFactory.createDirectPosition();
pos.setOrdinate(0, getNextNumber(tokenizer));
pos.setOrdinate(1, getNextNumber(tokenizer));
if (isNumberNext(tokenizer)) {
pos.setOrdinate(1, getNextNumber(tokenizer));
}
return pos;
}
private boolean isNumberNext(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(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(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(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(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(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(String expected, 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(StreamTokenizer tokenizer) {
switch (tokenizer.ttype) {
case StreamTokenizer.TT_NUMBER:
return "Point
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 Point
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(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 LineString
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 LineString
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(StreamTokenizer tokenizer) throws IOException, ParseException {
List coordList = getCoordinates(tokenizer);
LineString lineString = geometryFactory.createLineString(coordList);
List curveSegmentList = Collections.singletonList(lineString);
Curve curve = primitiveFactory.createCurve(curveSegmentList);
return curve;
}
/**
* Creates a Curve
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 Curve
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 Curve
* do not form a closed linestring, or if an unexpected token was
* encountered
*/
private Curve readLinearRingText(StreamTokenizer tokenizer)
throws IOException, ParseException {
List coordList = getCoordinates(tokenizer);
LineString lineString = geometryFactory.createLineString(coordList);
List curveSegmentList = Collections.singletonList(lineString);
Curve curve = primitiveFactory.createCurve(curveSegmentList);
return curve;
//List curveList = Collections.singletonList(curve);
//return primitiveFactory.createRing(curveList);
}
/**
* Creates an array of Point
s having the given Coordinate
s.
*
* @param coordinates the Coordinate
s with which to create the
* Point
s
* @return Point
s created using this WKTReader
* s GeometryFactory
*/
private List toPoints(List coordinates) {
List points = new ArrayList();
for (int i = 0; i < coordinates.size(); i++) {
points.add(positionFactory.createPosition((Point)coordinates.get(i)));
}
return points;
}
/**
* Creates a SurfaceBoundary
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 Surface
specified by the vertices in the stream
* @throws ParseException if the coordinates used to create the Polygon
* 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(StreamTokenizer tokenizer) throws IOException, ParseException {
String nextToken = getNextEmptyOrOpener(tokenizer);
if (nextToken.equals(EMPTY)) {
return null;
}
Curve curve = readLinearRingText(tokenizer);
List curveList = Collections.singletonList(curve);
Ring shell = primitiveFactory.createRing(curveList);
//Ring shell = readLinearRingText(tokenizer);
List holes = new ArrayList();
nextToken = getNextCloserOrComma(tokenizer);
while (nextToken.equals(COMMA)) {
Curve holecurve = readLinearRingText(tokenizer);
List holeList = Collections.singletonList(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 MultiPrimitive
specified by the next token
* in the stream
* @throws ParseException if the coordinates used to create the Polygon
* 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(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 MultiPrimitive
specified by the next token
* in the stream
* @throws ParseException if the coordinates used to create the Polygon
* 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(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 MultiPrimitive
specified by the next tokens
* in the stream
* @throws ParseException
* @throws IOException if an I/O error occurs
*/
private MultiPrimitive readGeometryCollectionText(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 MultiPrimitive
specified by the next tokens
* in the stream
* @throws ParseException
* @throws IOException if an I/O error occurs
*/
private MultiPrimitive readMultiLineStringText(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;
}
}