4
* The JTS Topology Suite is a collection of Java classes that
5
* implement the fundamental operations required to validate a given
6
* geo-spatial data set to a known topological specification.
8
* Copyright (C) 2001 Vivid Solutions
10
* This library is free software; you can redistribute it and/or
11
* modify it under the terms of the GNU Lesser General Public
12
* License as published by the Free Software Foundation; either
13
* version 2.1 of the License, or (at your option) any later version.
15
* This library is distributed in the hope that it will be useful,
16
* but WITHOUT ANY WARRANTY; without even the implied warranty of
17
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
* Lesser General Public License for more details.
20
* You should have received a copy of the GNU Lesser General Public
21
* License along with this library; if not, write to the Free Software
22
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24
* For more information, contact:
28
* 2328 Government Street
33
* www.vividsolutions.com
35
package com.vividsolutions.jts.io;
37
import com.vividsolutions.jts.geom.*;
38
import com.vividsolutions.jts.io.ParseException;
40
import com.vividsolutions.jts.util.Assert;
41
import java.io.IOException;
42
import java.io.Reader;
43
import java.io.StreamTokenizer;
44
import java.io.StringReader;
45
import java.util.ArrayList;
48
* Converts a Well-Known Text string to a <code>Geometry</code>.
50
* The <code>WKTReader</code> allows
51
* extracting <code>Geometry</code> objects from either input streams or
52
* internal strings. This allows it to function as a parser to read <code>Geometry</code>
53
* objects from text blocks embedded in other data formats (e.g. XML). <P>
56
* Text format is defined in the <A HREF="http://www.opengis.org/techno/specs.htm">
57
* OpenGIS Simple Features Specification for SQL</A> . <P>
59
* <B>Note: </B> There is an inconsistency in the SFS. The WKT grammar states
60
* that <code>MultiPoints</code> are represented by <code>MULTIPOINT ( ( x y), (x y) )</code>
61
* , but the examples show <code>MultiPoint</code>s as <code>MULTIPOINT ( x y, x y )</code>
62
* . Other implementations follow the latter syntax, so JTS will adopt it as
65
* A <code>WKTReader</code> is parameterized by a <code>GeometryFactory</code>
66
* , to allow it to create <code>Geometry</code> objects of the appropriate
67
* implementation. In particular, the <code>GeometryFactory</code> will
68
* determine the <code>PrecisionModel</code> and <code>SRID</code> that is
71
* The <code>WKTReader</code> will convert the input numbers to the precise
72
* internal representation.
74
* Reads non-standard "LINEARRING" tags.
78
public class WKTReader {
79
private GeometryFactory geometryFactory;
80
private PrecisionModel precisionModel;
83
* Creates a WKTReader that creates objects using a basic GeometryFactory.
86
this(new GeometryFactory());
90
* Creates a <code>WKTReader</code> that creates objects using the given
91
* <code>GeometryFactory</code>.
93
*@param geometryFactory the factory used to create <code>Geometry</code>s.
95
public WKTReader(GeometryFactory geometryFactory) {
96
this.geometryFactory = geometryFactory;
97
precisionModel = geometryFactory.getPrecisionModel();
103
* Converts a Well-known Text representation to a <code>Geometry</code>.
105
* @param wellKnownText
106
* one or more <Geometry Tagged Text>strings (see the OpenGIS
107
* Simple Features Specification) separated by whitespace
108
* @return a <code>Geometry</code> specified by <code>wellKnownText</code>
109
* @throws ParseException
110
* if a parsing problem occurs
112
public Geometry read(String wellKnownText) throws ParseException {
113
StringReader reader = new StringReader(wellKnownText);
123
* Converts a Well-known Text representation to a <code>Geometry</code>.
125
*@param reader a Reader which will return a <Geometry Tagged Text>
126
* string (see the OpenGIS Simple Features Specification)
127
*@return a <code>Geometry</code> read from <code>reader</code>
128
*@throws ParseException if a parsing problem occurs
130
public Geometry read(Reader reader) throws ParseException {
131
StreamTokenizer tokenizer = new StreamTokenizer(reader);
133
return readGeometryTaggedText(tokenizer);
135
catch (IOException e) {
136
throw new ParseException(e.toString());
141
* Returns the next array of <code>Coordinate</code>s in the stream.
143
*@param tokenizer tokenizer over a stream of text in Well-known Text
144
* format. The next element returned by the stream should be "(" (the
145
* beginning of "(x1 y1, x2 y2, ..., xn yn)") or "EMPTY".
146
*@return the next array of <code>Coordinate</code>s in the
147
* stream, or an empty array if "EMPTY" is the next element returned by
149
*@throws IOException if an I/O error occurs
150
*@throws ParseException if an unexpected token was encountered
152
private Coordinate[] getCoordinates(StreamTokenizer tokenizer)
153
throws IOException, ParseException
155
String nextToken = getNextEmptyOrOpener(tokenizer);
156
if (nextToken.equals("EMPTY")) {
157
return new Coordinate[]{};
159
ArrayList coordinates = new ArrayList();
160
coordinates.add(getPreciseCoordinate(tokenizer));
161
nextToken = getNextCloserOrComma(tokenizer);
162
while (nextToken.equals(",")) {
163
coordinates.add(getPreciseCoordinate(tokenizer));
164
nextToken = getNextCloserOrComma(tokenizer);
166
Coordinate[] array = new Coordinate[coordinates.size()];
167
return (Coordinate[]) coordinates.toArray(array);
170
private Coordinate getPreciseCoordinate(StreamTokenizer tokenizer)
171
throws IOException, ParseException
173
Coordinate coord = new Coordinate();
174
coord.x = getNextNumber(tokenizer);
175
coord.y = getNextNumber(tokenizer);
176
if (isNumberNext(tokenizer)) {
177
coord.z = getNextNumber(tokenizer);
179
precisionModel.makePrecise(coord);
182
private boolean isNumberNext(StreamTokenizer tokenizer) throws IOException {
184
return tokenizer.nextToken() == StreamTokenizer.TT_NUMBER;
187
tokenizer.pushBack();
191
* Returns the next number in the stream.
193
*@param tokenizer tokenizer over a stream of text in Well-known Text
194
* format. The next token must be a number.
195
*@return the next number in the stream
196
*@throws ParseException if the next token is not a number
197
*@throws IOException if an I/O error occurs
199
private double getNextNumber(StreamTokenizer tokenizer) throws IOException,
201
int type = tokenizer.nextToken();
203
case StreamTokenizer.TT_EOF:
204
throw new ParseException("Expected number but encountered end of stream");
205
case StreamTokenizer.TT_EOL:
206
throw new ParseException("Expected number but encountered end of line");
207
case StreamTokenizer.TT_NUMBER:
208
return tokenizer.nval;
209
case StreamTokenizer.TT_WORD:
210
throw new ParseException("Expected number but encountered word: " +
213
throw new ParseException("Expected number but encountered '('");
215
throw new ParseException("Expected number but encountered ')'");
217
throw new ParseException("Expected number but encountered ','");
219
Assert.shouldNeverReachHere("Encountered unexpected StreamTokenizer type: "
225
* Returns the next "EMPTY" or "(" in the stream as uppercase text.
227
*@param tokenizer tokenizer over a stream of text in Well-known Text
228
* format. The next token must be "EMPTY" or "(".
229
*@return the next "EMPTY" or "(" in the stream as uppercase
231
*@throws ParseException if the next token is not "EMPTY" or "("
232
*@throws IOException if an I/O error occurs
234
private String getNextEmptyOrOpener(StreamTokenizer tokenizer) throws IOException, ParseException {
235
String nextWord = getNextWord(tokenizer);
236
if (nextWord.equals("EMPTY") || nextWord.equals("(")) {
239
throw new ParseException("Expected 'EMPTY' or '(' but encountered '" +
244
* Returns the next ")" or "," in the stream.
246
*@param tokenizer tokenizer over a stream of text in Well-known Text
247
* format. The next token must be ")" or ",".
248
*@return the next ")" or "," in the stream
249
*@throws ParseException if the next token is not ")" or ","
250
*@throws IOException if an I/O error occurs
252
private String getNextCloserOrComma(StreamTokenizer tokenizer) throws IOException, ParseException {
253
String nextWord = getNextWord(tokenizer);
254
if (nextWord.equals(",") || nextWord.equals(")")) {
257
throw new ParseException("Expected ')' or ',' but encountered '" + nextWord
262
* Returns the next ")" in the stream.
264
*@param tokenizer tokenizer over a stream of text in Well-known Text
265
* format. The next token must be ")".
266
*@return the next ")" in the stream
267
*@throws ParseException if the next token is not ")"
268
*@throws IOException if an I/O error occurs
270
private String getNextCloser(StreamTokenizer tokenizer) throws IOException, ParseException {
271
String nextWord = getNextWord(tokenizer);
272
if (nextWord.equals(")")) {
275
throw new ParseException("Expected ')' but encountered '" + nextWord + "'");
279
* Returns the next word in the stream as uppercase text.
281
*@param tokenizer tokenizer over a stream of text in Well-known Text
282
* format. The next token must be a word.
283
*@return the next word in the stream as uppercase text
284
*@throws ParseException if the next token is not a word
285
*@throws IOException if an I/O error occurs
287
private String getNextWord(StreamTokenizer tokenizer) throws IOException, ParseException {
288
int type = tokenizer.nextToken();
290
case StreamTokenizer.TT_EOF:
291
throw new ParseException("Expected word but encountered end of stream");
292
case StreamTokenizer.TT_EOL:
293
throw new ParseException("Expected word but encountered end of line");
294
case StreamTokenizer.TT_NUMBER:
295
throw new ParseException("Expected word but encountered number: " +
297
case StreamTokenizer.TT_WORD:
298
return tokenizer.sval.toUpperCase();
306
Assert.shouldNeverReachHere("Encountered unexpected StreamTokenizer type: " + type);
311
* Creates a <code>Geometry</code> using the next token in the stream.
313
*@param tokenizer tokenizer over a stream of text in Well-known Text
314
* format. The next tokens must form a <Geometry Tagged Text>.
315
*@return a <code>Geometry</code> specified by the next token
317
*@throws ParseException if the coordinates used to create a <code>Polygon</code>
318
* shell and holes do not form closed linestrings, or if an unexpected
319
* token was encountered
320
*@throws IOException if an I/O error occurs
322
private Geometry readGeometryTaggedText(StreamTokenizer tokenizer) throws IOException, ParseException {
323
String type = getNextWord(tokenizer);
324
if (type.equals("POINT")) {
325
return readPointText(tokenizer);
327
else if (type.equals("LINESTRING")) {
328
return readLineStringText(tokenizer);
330
else if (type.equals("LINEARRING")) {
331
return readLinearRingText(tokenizer);
333
else if (type.equals("POLYGON")) {
334
return readPolygonText(tokenizer);
336
else if (type.equals("MULTIPOINT")) {
337
return readMultiPointText(tokenizer);
339
else if (type.equals("MULTILINESTRING")) {
340
return readMultiLineStringText(tokenizer);
342
else if (type.equals("MULTIPOLYGON")) {
343
return readMultiPolygonText(tokenizer);
345
else if (type.equals("GEOMETRYCOLLECTION")) {
346
return readGeometryCollectionText(tokenizer);
348
throw new ParseException("Unknown type: " + type);
352
* Creates a <code>Point</code> using the next token in the stream.
354
*@param tokenizer tokenizer over a stream of text in Well-known Text
355
* format. The next tokens must form a <Point Text>.
356
*@return a <code>Point</code> specified by the next token in
358
*@throws IOException if an I/O error occurs
359
*@throws ParseException if an unexpected token was encountered
361
private Point readPointText(StreamTokenizer tokenizer) throws IOException, ParseException {
362
String nextToken = getNextEmptyOrOpener(tokenizer);
363
if (nextToken.equals("EMPTY")) {
364
return geometryFactory.createPoint((Coordinate)null);
366
Point point = geometryFactory.createPoint(getPreciseCoordinate(tokenizer));
367
getNextCloser(tokenizer);
372
* Creates a <code>LineString</code> using the next token in the stream.
374
*@param tokenizer tokenizer over a stream of text in Well-known Text
375
* format. The next tokens must form a <LineString Text>.
376
*@return a <code>LineString</code> specified by the next
377
* token in the stream
378
*@throws IOException if an I/O error occurs
379
*@throws ParseException if an unexpected token was encountered
381
private LineString readLineStringText(StreamTokenizer tokenizer) throws IOException, ParseException {
382
return geometryFactory.createLineString(getCoordinates(tokenizer));
386
* Creates a <code>LinearRing</code> using the next token in the stream.
388
*@param tokenizer tokenizer over a stream of text in Well-known Text
389
* format. The next tokens must form a <LineString Text>.
390
*@return a <code>LinearRing</code> specified by the next
391
* token in the stream
392
*@throws IOException if an I/O error occurs
393
*@throws ParseException if the coordinates used to create the <code>LinearRing</code>
394
* do not form a closed linestring, or if an unexpected token was
397
private LinearRing readLinearRingText(StreamTokenizer tokenizer)
398
throws IOException, ParseException
400
return geometryFactory.createLinearRing(getCoordinates(tokenizer));
404
* Creates a <code>MultiPoint</code> using the next token in the stream.
406
*@param tokenizer tokenizer over a stream of text in Well-known Text
407
* format. The next tokens must form a <MultiPoint Text>.
408
*@return a <code>MultiPoint</code> specified by the next
409
* token in the stream
410
*@throws IOException if an I/O error occurs
411
*@throws ParseException if an unexpected token was encountered
413
private MultiPoint readMultiPointText(StreamTokenizer tokenizer) throws IOException, ParseException {
414
return geometryFactory.createMultiPoint(toPoints(getCoordinates(tokenizer)));
418
* Creates an array of <code>Point</code>s having the given <code>Coordinate</code>
421
*@param coordinates the <code>Coordinate</code>s with which to create the
422
* <code>Point</code>s
423
*@return <code>Point</code>s created using this <code>WKTReader</code>
424
* s <code>GeometryFactory</code>
426
private Point[] toPoints(Coordinate[] coordinates) {
427
ArrayList points = new ArrayList();
428
for (int i = 0; i < coordinates.length; i++) {
429
points.add(geometryFactory.createPoint(coordinates[i]));
431
return (Point[]) points.toArray(new Point[]{});
435
* Creates a <code>Polygon</code> using the next token in the stream.
437
*@param tokenizer tokenizer over a stream of text in Well-known Text
438
* format. The next tokens must form a <Polygon Text>.
439
*@return a <code>Polygon</code> specified by the next token
441
*@throws ParseException if the coordinates used to create the <code>Polygon</code>
442
* shell and holes do not form closed linestrings, or if an unexpected
443
* token was encountered.
444
*@throws IOException if an I/O error occurs
446
private Polygon readPolygonText(StreamTokenizer tokenizer) throws IOException, ParseException {
447
String nextToken = getNextEmptyOrOpener(tokenizer);
448
if (nextToken.equals("EMPTY")) {
449
return geometryFactory.createPolygon(geometryFactory.createLinearRing(
450
new Coordinate[]{}), new LinearRing[]{});
452
ArrayList holes = new ArrayList();
453
LinearRing shell = readLinearRingText(tokenizer);
454
nextToken = getNextCloserOrComma(tokenizer);
455
while (nextToken.equals(",")) {
456
LinearRing hole = readLinearRingText(tokenizer);
458
nextToken = getNextCloserOrComma(tokenizer);
460
LinearRing[] array = new LinearRing[holes.size()];
461
return geometryFactory.createPolygon(shell, (LinearRing[]) holes.toArray(array));
465
* Creates a <code>MultiLineString</code> using the next token in the stream.
467
*@param tokenizer tokenizer over a stream of text in Well-known Text
468
* format. The next tokens must form a <MultiLineString Text>.
469
*@return a <code>MultiLineString</code> specified by the
470
* next token in the stream
471
*@throws IOException if an I/O error occurs
472
*@throws ParseException if an unexpected token was encountered
474
private com.vividsolutions.jts.geom.MultiLineString readMultiLineStringText(StreamTokenizer tokenizer) throws IOException, ParseException {
475
String nextToken = getNextEmptyOrOpener(tokenizer);
476
if (nextToken.equals("EMPTY")) {
477
return geometryFactory.createMultiLineString(new LineString[]{});
479
ArrayList lineStrings = new ArrayList();
480
LineString lineString = readLineStringText(tokenizer);
481
lineStrings.add(lineString);
482
nextToken = getNextCloserOrComma(tokenizer);
483
while (nextToken.equals(",")) {
484
lineString = readLineStringText(tokenizer);
485
lineStrings.add(lineString);
486
nextToken = getNextCloserOrComma(tokenizer);
488
LineString[] array = new LineString[lineStrings.size()];
489
return geometryFactory.createMultiLineString((LineString[]) lineStrings.toArray(array));
493
* Creates a <code>MultiPolygon</code> using the next token in the stream.
495
*@param tokenizer tokenizer over a stream of text in Well-known Text
496
* format. The next tokens must form a <MultiPolygon Text>.
497
*@return a <code>MultiPolygon</code> specified by the next
498
* token in the stream, or if if the coordinates used to create the
499
* <code>Polygon</code> shells and holes do not form closed linestrings.
500
*@throws IOException if an I/O error occurs
501
*@throws ParseException if an unexpected token was encountered
503
private MultiPolygon readMultiPolygonText(StreamTokenizer tokenizer) throws IOException, ParseException {
504
String nextToken = getNextEmptyOrOpener(tokenizer);
505
if (nextToken.equals("EMPTY")) {
506
return geometryFactory.createMultiPolygon(new Polygon[]{});
508
ArrayList polygons = new ArrayList();
509
Polygon polygon = readPolygonText(tokenizer);
510
polygons.add(polygon);
511
nextToken = getNextCloserOrComma(tokenizer);
512
while (nextToken.equals(",")) {
513
polygon = readPolygonText(tokenizer);
514
polygons.add(polygon);
515
nextToken = getNextCloserOrComma(tokenizer);
517
Polygon[] array = new Polygon[polygons.size()];
518
return geometryFactory.createMultiPolygon((Polygon[]) polygons.toArray(array));
522
* Creates a <code>GeometryCollection</code> using the next token in the
525
*@param tokenizer tokenizer over a stream of text in Well-known Text
526
* format. The next tokens must form a <GeometryCollection Text>.
527
*@return a <code>GeometryCollection</code> specified by the
528
* next token in the stream
529
*@throws ParseException if the coordinates used to create a <code>Polygon</code>
530
* shell and holes do not form closed linestrings, or if an unexpected
531
* token was encountered
532
*@throws IOException if an I/O error occurs
534
private GeometryCollection readGeometryCollectionText(StreamTokenizer tokenizer) throws IOException, ParseException {
535
String nextToken = getNextEmptyOrOpener(tokenizer);
536
if (nextToken.equals("EMPTY")) {
537
return geometryFactory.createGeometryCollection(new Geometry[]{});
539
ArrayList geometries = new ArrayList();
540
Geometry geometry = readGeometryTaggedText(tokenizer);
541
geometries.add(geometry);
542
nextToken = getNextCloserOrComma(tokenizer);
543
while (nextToken.equals(",")) {
544
geometry = readGeometryTaggedText(tokenizer);
545
geometries.add(geometry);
546
nextToken = getNextCloserOrComma(tokenizer);
548
Geometry[] array = new Geometry[geometries.size()];
549
return geometryFactory.createGeometryCollection((Geometry[]) geometries.toArray(array));