View Javadoc
1   /*******************************************************************************
2    * Copyright (c) 2015 VoyagerSearch and others
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Apache License, Version 2.0 which
5    * accompanies this distribution and is available at
6    *    http://www.apache.org/licenses/LICENSE-2.0.txt
7    ******************************************************************************/
8   
9   package org.locationtech.spatial4j.io;
10  
11  import org.locationtech.spatial4j.context.SpatialContext;
12  import org.locationtech.spatial4j.context.SpatialContextFactory;
13  import org.locationtech.spatial4j.distance.DistanceUtils;
14  import org.locationtech.spatial4j.exception.InvalidShapeException;
15  import org.locationtech.spatial4j.shape.Circle;
16  import org.locationtech.spatial4j.shape.Point;
17  import org.locationtech.spatial4j.shape.Shape;
18  import org.locationtech.spatial4j.shape.ShapeFactory;
19  import org.noggit.JSONParser;
20  
21  import java.io.IOException;
22  import java.io.Reader;
23  import java.io.StringReader;
24  import java.text.ParseException;
25  import java.util.ArrayList;
26  import java.util.List;
27  
28  public class GeoJSONReader implements ShapeReader {
29  
30    protected static final String BUFFER = "buffer";
31    protected static final String BUFFER_UNITS = "buffer_units";
32  
33    protected final SpatialContext ctx;
34    protected final ShapeFactory shapeFactory;
35  
36    public GeoJSONReader(SpatialContext ctx, SpatialContextFactory factory) {
37      this.ctx = ctx;
38      this.shapeFactory = ctx.getShapeFactory();
39    }
40  
41    @Override
42    public String getFormatName() {
43      return ShapeIO.GeoJSON;
44    }
45  
46    @Override
47    public final Shape read(Reader reader) throws IOException, ParseException {
48      return readShape(new JSONParser(reader));
49    }
50  
51    @Override
52    public Shape read(Object value) throws IOException, ParseException, InvalidShapeException {
53      String v = value.toString().trim();
54      return read(new StringReader(v));
55    }
56  
57    @Override
58    public Shape readIfSupported(Object value) throws InvalidShapeException {
59      String v = value.toString().trim();
60      if (!(v.startsWith("{") && v.endsWith("}"))) {
61        return null;
62      }
63      try {
64        return read(new StringReader(v));
65      } catch (IOException ex) {
66      } catch (ParseException e) {
67      }
68      return null;
69    }
70  
71    // --------------------------------------------------------------
72    // Read GeoJSON
73    // --------------------------------------------------------------
74  
75  
76    protected void readCoordXYZ(JSONParser parser, ShapeFactory.PointsBuilder pointsBuilder) throws IOException, ParseException {
77      assert (parser.lastEvent() == JSONParser.ARRAY_START);
78  
79      double x = Double.NaN, y = Double.NaN, z = Double.NaN;
80      int idx = 0;
81  
82      int evt = parser.nextEvent();
83      while (evt != JSONParser.EOF) {
84        switch (evt) {
85          case JSONParser.LONG:
86          case JSONParser.NUMBER:
87          case JSONParser.BIGNUMBER:
88            double value = parser.getDouble();
89            switch(idx) {
90              case 0: x = value; break;
91              case 1: y = value; break;
92              case 2: z = value; break;
93            }
94            idx++;
95            break;
96  
97          case JSONParser.ARRAY_END:
98            if (idx <= 2) { // don't have a 'z'
99              pointsBuilder.pointXY(shapeFactory.normX(x), shapeFactory.normY(y));
100           } else {
101             pointsBuilder.pointXYZ(shapeFactory.normX(x), shapeFactory.normY(y), shapeFactory.normZ(z));
102           }
103           return;
104 
105         case JSONParser.STRING:
106         case JSONParser.BOOLEAN:
107         case JSONParser.NULL:
108         case JSONParser.OBJECT_START:
109         case JSONParser.OBJECT_END:
110         case JSONParser.ARRAY_START:
111         default:
112           throw new ParseException("Unexpected " + JSONParser.getEventString(evt),
113               (int) parser.getPosition());
114       }
115       evt = parser.nextEvent();
116     }
117     return;
118   }
119 
120   protected void readCoordListXYZ(JSONParser parser, ShapeFactory.PointsBuilder pointsBuilder) throws IOException, ParseException {
121     assert (parser.lastEvent() == JSONParser.ARRAY_START);
122 
123     int evt = parser.nextEvent();
124     while (evt != JSONParser.EOF) {
125       switch (evt) {
126         case JSONParser.ARRAY_START:
127           readCoordXYZ(parser, pointsBuilder); // reads until ARRAY_END
128           break;
129 
130         case JSONParser.ARRAY_END:
131           return;
132 
133         default:
134           throw new ParseException("Unexpected " + JSONParser.getEventString(evt),
135               (int) parser.getPosition());
136       }
137       evt = parser.nextEvent();
138     }
139   }
140 
141   protected void readUntilEvent(JSONParser parser, final int event) throws IOException {
142     int evt = parser.lastEvent();
143     while (true) {
144       if (evt == event || evt == JSONParser.EOF) {
145         return;
146       }
147       evt = parser.nextEvent();
148     }
149   }
150 
151   protected Shape readPoint(JSONParser parser) throws IOException, ParseException {
152     assert (parser.lastEvent() == JSONParser.ARRAY_START);
153     OnePointsBuilder onePointsBuilder = new OnePointsBuilder(shapeFactory);
154     readCoordXYZ(parser, onePointsBuilder);
155     Point point = onePointsBuilder.getPoint();
156     readUntilEvent(parser, JSONParser.OBJECT_END);
157     return point;
158   }
159 
160   protected Shape readLineString(JSONParser parser) throws IOException, ParseException {
161     assert (parser.lastEvent() == JSONParser.ARRAY_START);
162     ShapeFactory.LineStringBuilder builder = shapeFactory.lineString();
163     readCoordListXYZ(parser, builder);
164 
165     // check for buffer field
166     builder.buffer(readDistance(BUFFER, BUFFER_UNITS, parser));
167 
168     Shape out = builder.build();
169     readUntilEvent(parser, JSONParser.OBJECT_END);
170     return out;
171   }
172 
173   protected Circle readCircle(JSONParser parser) throws IOException, ParseException {
174     assert (parser.lastEvent() == JSONParser.ARRAY_START);
175     OnePointsBuilder onePointsBuilder = new OnePointsBuilder(shapeFactory);
176     readCoordXYZ(parser, onePointsBuilder);
177     Point point = onePointsBuilder.getPoint();
178 
179     return shapeFactory.circle(point, readDistance("radius", "radius_units", parser));
180   }
181 
182   /**
183    * Helper method to read a up until a distance value (radius, buffer) and it's corresponding unit are found.
184    * <p>
185    * This method returns 0 if no distance value is found. This method currently only handles distance units of "km".
186    * </p>
187    * @param distProperty The name of the property containing the distance value.
188    * @param distUnitsProperty The name of the property containing the distance unit. 
189    */
190   protected double readDistance(String distProperty, String distUnitsProperty, JSONParser parser) throws IOException {
191     double dist = 0;
192 
193     String key = null;
194 
195     int event = JSONParser.OBJECT_END;
196     int evt = parser.lastEvent();
197     while (true) {
198       if (evt == event || evt == JSONParser.EOF) {
199         break;
200       }
201       evt = parser.nextEvent();
202       if(parser.wasKey()) {
203         key = parser.getString();
204       }
205       else if(evt==JSONParser.NUMBER || evt==JSONParser.LONG) {
206         if(distProperty.equals(key)) {
207           dist = parser.getDouble();
208         }
209       }
210       else if(evt==JSONParser.STRING) {
211         if(distUnitsProperty.equals(key)) {
212           String units = parser.getString();
213           //TODO: support for more units?
214           if("km".equals(units)) {
215             // Convert KM to degrees
216             dist =
217                 DistanceUtils.dist2Degrees(dist, DistanceUtils.EARTH_MEAN_RADIUS_KM);
218           }
219         }
220       }
221     }
222 
223     return shapeFactory.normDist(dist);
224   }
225 
226   protected Shape readShape(JSONParser parser) throws IOException, ParseException {
227     String type = null;
228 
229     String key = null;
230     int evt = parser.nextEvent();
231     while (evt != JSONParser.EOF) {
232       switch (evt) {
233         case JSONParser.STRING:
234           if (parser.wasKey()) {
235             key = parser.getString();
236           } else {
237             if ("type".equals(key)) {
238               type = parser.getString();
239             } else {
240               throw new ParseException("Unexpected String Value for key: " + key,
241                       (int) parser.getPosition());
242             }
243           }
244           break;
245 
246         case JSONParser.ARRAY_START:
247           if ("coordinates".equals(key)) {
248             Shape shape = readShapeFromCoordinates(type, parser);
249             readUntilEvent(parser, JSONParser.OBJECT_END);
250             return shape;
251           } else if ("geometries".equals(key)) {
252             List<Shape> shapes = new ArrayList<>();
253             int sub = parser.nextEvent();
254             while (sub != JSONParser.EOF) {
255               if (sub == JSONParser.OBJECT_START) {
256                 Shape s = readShape(parser);
257                 if (s != null) {
258                   shapes.add(s);
259                 }
260               } else if (sub == JSONParser.OBJECT_END) {
261                 break;
262               }
263               sub = parser.nextEvent();
264             }
265             return ctx.makeCollection(shapes);
266           }
267           else {
268             throw new ParseException("Unknown type: "+type,
269                 (int) parser.getPosition());
270           }
271 
272         case JSONParser.ARRAY_END:
273           break;
274 
275         case JSONParser.OBJECT_START:
276           if (key != null) {
277            // System.out.println("Unexpected object: " + key);
278           }
279           break;
280 
281         case JSONParser.LONG:
282         case JSONParser.NUMBER:
283         case JSONParser.BIGNUMBER:
284         case JSONParser.BOOLEAN:
285         case JSONParser.NULL:
286         case JSONParser.OBJECT_END:
287          // System.out.println(">>>>>" + JSONParser.getEventString(evt) + " :: " + key);
288           break;
289 
290         default:
291           throw new ParseException("Unexpected " + JSONParser.getEventString(evt),
292               (int) parser.getPosition());
293       }
294       evt = parser.nextEvent();
295     }
296     throw new RuntimeException("unable to parse shape");
297   }
298 
299   protected Shape readShapeFromCoordinates(String type, JSONParser parser) throws IOException, ParseException {
300     switch(type) {
301       case "Point":
302         return readPoint(parser);
303       case "LineString":
304         return readLineString(parser);
305       case "Circle":
306         return readCircle(parser);
307       case "Polygon":
308         return readPolygon(parser, shapeFactory.polygon()).buildOrRect();
309       case "MultiPoint":
310         return readMultiPoint(parser);
311       case "MultiLineString":
312         return readMultiLineString(parser);
313       case "MultiPolygon":
314         return readMultiPolygon(parser);
315       default:
316         throw new ParseException("Unable to make shape type: " + type,
317                 (int) parser.getPosition());
318     }
319   }
320 
321   protected ShapeFactory.PolygonBuilder readPolygon(JSONParser parser, ShapeFactory.PolygonBuilder polygonBuilder) throws IOException, ParseException {
322     assert (parser.lastEvent() == JSONParser.ARRAY_START);
323     boolean firstRing = true;
324     int evt = parser.nextEvent();
325     while (true) {
326       switch (evt) {
327         case JSONParser.ARRAY_START:
328           if (firstRing) {
329             readCoordListXYZ(parser, polygonBuilder);
330             firstRing = false;
331           } else {
332             ShapeFactory.PolygonBuilder.HoleBuilder holeBuilder = polygonBuilder.hole();
333             readCoordListXYZ(parser, holeBuilder);
334             holeBuilder.endHole();
335           }
336           break;
337         case JSONParser.ARRAY_END:
338           return polygonBuilder;
339         default:
340           throw new ParseException("Unexpected " + JSONParser.getEventString(evt),
341                   (int) parser.getPosition());
342       }
343       evt = parser.nextEvent();
344     }
345   }
346 
347   protected Shape readMultiPoint(JSONParser parser) throws IOException, ParseException {
348     assert (parser.lastEvent() == JSONParser.ARRAY_START);
349     ShapeFactory.MultiPointBuilder builder = shapeFactory.multiPoint();
350     readCoordListXYZ(parser, builder);
351     return builder.build();
352   }
353 
354   protected Shape readMultiLineString(JSONParser parser) throws IOException, ParseException {
355     assert (parser.lastEvent() == JSONParser.ARRAY_START);
356     // TODO need Spatial4j LineString interface
357     ShapeFactory.MultiLineStringBuilder builder = shapeFactory.multiLineString();
358     int evt = parser.nextEvent();
359     while (true) {
360       switch (evt) {
361         case JSONParser.ARRAY_START:
362           ShapeFactory.LineStringBuilder lineStringBuilder = builder.lineString();
363           readCoordListXYZ(parser, lineStringBuilder);
364           builder.add(lineStringBuilder);
365           break;
366         case JSONParser.ARRAY_END:
367           return builder.build();
368         default:
369           throw new ParseException("Unexpected " + JSONParser.getEventString(evt),
370                   (int) parser.getPosition());
371       }
372       evt = parser.nextEvent();
373     }
374   }
375 
376   protected Shape readMultiPolygon(JSONParser parser) throws IOException, ParseException {
377     assert (parser.lastEvent() == JSONParser.ARRAY_START);
378     // TODO need Spatial4j Polygon interface
379     ShapeFactory.MultiPolygonBuilder builder = shapeFactory.multiPolygon();
380     int evt = parser.nextEvent();
381     while (true) {
382       switch (evt) {
383         case JSONParser.ARRAY_START:
384           ShapeFactory.PolygonBuilder polygonBuilder = readPolygon(parser, builder.polygon());
385           builder.add(polygonBuilder);
386           break;
387         case JSONParser.ARRAY_END:
388           return builder.build();
389         default:
390           throw new ParseException("Unexpected " + JSONParser.getEventString(evt),
391                   (int) parser.getPosition());
392       }
393       evt = parser.nextEvent();
394     }
395   }
396 }