com.vividsolutions.jts.io.WKTReader Maven / Gradle / Ivy
The newest version!
/*
* The JTS Topology Suite is a collection of Java classes that
* implement the fundamental operations required to validate a given
* geo-spatial data set to a known topological specification.
*
* Copyright (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; either
* version 2.1 of the License, or (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* For more information, contact:
*
* Vivid Solutions
* Suite #1A
* 2328 Government Street
* Victoria BC V8T 5G5
* Canada
*
* (250)385-6040
* www.vividsolutions.com
*/
package com.vividsolutions.jts.io;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.util.*;
import com.vividsolutions.jts.io.ParseException;
import java.io.IOException;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
/**
* Converts a geometry in Well-Known Text format to a {@link Geometry}.
*
* WKTReader
supports
* extracting Geometry
objects from either {@link Reader}s or
* {@link String}s. This allows it to function as a parser to read Geometry
* objects from text blocks embedded in other data formats (e.g. XML).
*
* A WKTReader
is parameterized by a GeometryFactory
,
* to allow it to create Geometry
objects of the appropriate
* implementation. In particular, the GeometryFactory
* determines the PrecisionModel
and SRID
that is
* used.
*
* The WKTReader
converts all input numbers to the precise
* internal representation.
*
*
Notes:
*
* - Keywords are case-insensitive.
*
- The reader supports non-standard "LINEARRING" tags.
*
- The reader uses Double.parseDouble to perform the conversion of ASCII
* numbers to floating point. This means it supports the Java
* syntax for floating point literals (including scientific notation).
*
*
* Syntax
* The following syntax specification describes the version of Well-Known Text
* supported by JTS.
* (The specification uses a syntax language similar to that used in
* the C and Java language specifications.)
*
*
*
* WKTGeometry: one of
*
* WKTPoint WKTLineString WKTLinearRing WKTPolygon
* WKTMultiPoint WKTMultiLineString WKTMultiPolygon
* WKTGeometryCollection
*
* WKTPoint: POINT ( Coordinate )
*
* WKTLineString: LINESTRING CoordinateSequence
*
* WKTLinearRing: LINEARRING CoordinateSequence
*
* WKTPolygon: POLYGON CoordinateSequenceList
*
* WKTMultiPoint: MULTIPOINT CoordinateSingletonList
*
* WKTMultiLineString: MULTILINESTRING CoordinateSequenceList
*
* WKTMultiPolygon:
* MULTIPOLYGON ( CoordinateSequenceList { , CoordinateSequenceList } )
*
* WKTGeometryCollection:
* GEOMETRYCOLLECTION ( WKTGeometry { , WKTGeometry } )
*
* CoordinateSingletonList:
* ( CoordinateSingleton { , CoordinateSingleton } )
* | EMPTY
*
* CoordinateSingleton:
* ( Coordinate )
* | EMPTY
*
* CoordinateSequenceList:
* ( CoordinateSequence { , CoordinateSequence } )
* | EMPTY
*
* CoordinateSequence:
* ( Coordinate { , Coordinate } )
* | EMPTY
*
* Coordinate:
* Number Number Numberopt
*
* Number: A Java-style floating-point number (including NaN, with arbitrary case)
*
*
*
*
*@version 1.7
* @see WKTWriter
*/
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 static final String NAN_SYMBOL = "NaN";
private GeometryFactory geometryFactory;
private PrecisionModel precisionModel;
private StreamTokenizer tokenizer;
/**
* Creates a reader that creates objects using the default {@link GeometryFactory}.
*/
public WKTReader() {
this(new GeometryFactory());
}
/**
* Creates a reader that creates objects using the given
* {@link GeometryFactory}.
*
*@param geometryFactory the factory used to create Geometry
s.
*/
public WKTReader(GeometryFactory geometryFactory) {
this.geometryFactory = geometryFactory;
precisionModel = geometryFactory.getPrecisionModel();
}
/**
* Reads a Well-Known Text representation of a {@link Geometry}
* from a {@link String}.
*
* @param wellKnownText
* one or more strings (see the OpenGIS
* Simple Features Specification) separated by whitespace
* @return a Geometry
specified by wellKnownText
* @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
* 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 {
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 Coordinate
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 Coordinate
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[] getCoordinatesNoLeftParen() throws IOException, ParseException {
String nextToken = null;
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.
* NaN values are handled correctly, and
* the case of the "NaN" symbol 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) {
parseErrorWithLine("Invalid number: " + tokenizer.sval);
}
}
}
}
parseErrorExpected("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;
}
parseErrorExpected(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;
}
parseErrorExpected(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;
}
parseErrorExpected(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;
}
parseErrorExpected("word");
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 lookaheadWord() throws IOException, ParseException {
String nextWord = getNextWord();
tokenizer.pushBack();
return nextWord;
}
/**
* Throws a formatted ParseException reporting that the current token
* was unexpected.
*
* @param expected a description of what was expected
* @throws ParseException
* @throws AssertionFailedException if an invalid token is encountered
*/
private void parseErrorExpected(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();
parseErrorWithLine("Expected " + expected + " but found " + tokenStr);
}
private void parseErrorWithLine(String msg)
throws ParseException
{
throw new ParseException(msg + " (line " + tokenizer.lineno() + ")");
}
/**
* 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 "";
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 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 Geometry
specified by the next token
* 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() 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("MULTIPOLYGON")) {
return readMultiPolygonText();
}
else if (type.equalsIgnoreCase("GEOMETRYCOLLECTION")) {
return readGeometryCollectionText();
}
parseErrorWithLine("Unknown geometry type: " + type);
// should never reach here
return null;
}
/**
* Creates a 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() 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 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 LineString readLineStringText() throws IOException, ParseException {
return geometryFactory.createLineString(getCoordinates());
}
/**
* Creates a LinearRing
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 LinearRing
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 LinearRing
* do not form a closed linestring, or if an unexpected token was
* encountered
*/
private LinearRing readLinearRingText()
throws IOException, ParseException
{
return geometryFactory.createLinearRing(getCoordinates());
}
/*
private MultiPoint OLDreadMultiPointText() throws IOException, ParseException {
return geometryFactory.createMultiPoint(toPoints(getCoordinates()));
}
*/
private static final boolean ALLOW_OLD_JTS_MULTIPOINT_SYNTAX = true;
/**
* Creates a MultiPoint
using the next tokens in the stream.
*
*@param tokenizer tokenizer over a stream of text in Well-known Text
* format. The next tokens must form a <MultiPoint Text>.
*@return a MultiPoint
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
{
String nextToken = getNextEmptyOrOpener();
if (nextToken.equals(EMPTY)) {
return geometryFactory.createMultiPoint(new Point[0]);
}
// check for old-style JTS syntax and parse it if present
// MD 2009-02-21 - this is only provided for backwards compatibility for a few versions
if (ALLOW_OLD_JTS_MULTIPOINT_SYNTAX) {
String nextWord = lookaheadWord();
if (nextWord != L_PAREN) {
return geometryFactory.createMultiPoint(toPoints(getCoordinatesNoLeftParen()));
}
}
ArrayList points = new ArrayList();
Point point = readPointText();
points.add(point);
nextToken = getNextCloserOrComma();
while (nextToken.equals(COMMA)) {
point = readPointText();
points.add(point);
nextToken = getNextCloserOrComma();
}
Point[] array = new Point[points.size()];
return geometryFactory.createMultiPoint((Point[]) points.toArray(array));
}
/**
* 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 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 Polygon
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 Polygon
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 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));
}
/**
* Creates a MultiLineString
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 <MultiLineString Text>.
*@return a MultiLineString
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 MultiPolygon
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 <MultiPolygon Text>.
*@return a MultiPolygon
specified by the next
* token in the stream, or if if the coordinates used to create the
* Polygon
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 GeometryCollection
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 <GeometryCollection Text>.
*@return a GeometryCollection
specified by the
* next token 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 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));
}
}