
org.locationtech.jts.io.WKTReader Maven / Gradle / Ivy
/*
* Copyright (c) 2016 Vivid Solutions.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.locationtech.jts.io;
import java.io.IOException;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.util.Assert;
import org.locationtech.jts.util.AssertionFailedException;
/**
* 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.)
*
* As of version 1.15, JTS can read (but not write) WKT Strings including Z, M or ZM
* in the name of the geometry type (ex. POINT Z, LINESTRINGZM).
* Note that it only makes the reader more flexible, but JTS could already read
* 3D coordinates from WKT String and still can't read 4D coordinates.
*
*
* WKTGeometry: one of
*
* WKTPoint WKTLineString WKTLinearRing WKTPolygon
* WKTMultiPoint WKTMultiLineString WKTMultiPolygon
* WKTGeometryCollection
*
* WKTPoint: POINT[Dimension] ( Coordinate )
*
* WKTLineString: LINESTRING[Dimension] CoordinateSequence
*
* WKTLinearRing: LINEARRING[Dimension] CoordinateSequence
*
* WKTPolygon: POLYGON[Dimension] CoordinateSequenceList
*
* WKTMultiPoint: MULTIPOINT[Dimension] CoordinateSingletonList
*
* WKTMultiLineString: MULTILINESTRING[Dimension] CoordinateSequenceList
*
* WKTMultiPolygon:
* MULTIPOLYGON[Dimension] ( CoordinateSequenceList { , CoordinateSequenceList } )
*
* WKTGeometryCollection:
* GEOMETRYCOLLECTION[Dimension] ( 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)
*
* Dimension:
* Z| Z|M| M|ZM| ZM
*
*
*
*
*@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;
// Not yet used (useful if we want to read Z, M and ZM WKT)
private boolean z;
private boolean m;
/**
* 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 <Geometry Tagged Text> 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 <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 {
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('#');
z = false;
m = false;
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();
}
if (isNumberNext()) {
getNextNumber(); // ignore M value
}
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.equalsIgnoreCase("Z")) {
z = true;
nextWord = getNextWord();
}
else if (nextWord.equalsIgnoreCase("M")) {
m = true;
nextWord = getNextWord();
}
else if (nextWord.equalsIgnoreCase("ZM")) {
z = true;
m = true;
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;
try{
type = getNextWord().toUpperCase();
if (type.endsWith("Z")) z = true;
if (type.endsWith("M")) m = true;
}catch(IOException e){
return null;
}catch(ParseException e){
return null;
}
if (type.startsWith("POINT")) {
return readPointText();
}
else if (type.startsWith("LINESTRING")) {
return readLineStringText();
}
else if (type.startsWith("LINEARRING")) {
return readLinearRingText();
}
else if (type.startsWith("POLYGON")) {
return readPolygonText();
}
else if (type.startsWith("MULTIPOINT")) {
return readMultiPointText();
}
else if (type.startsWith("MULTILINESTRING")) {
return readMultiLineStringText();
}
else if (type.startsWith("MULTIPOLYGON")) {
return readMultiPolygonText();
}
else if (type.startsWith("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();
}
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 org.locationtech.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();
}
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));
}
}