/******************************************************************************* * Copyright (c) 2015 VoyagerSearch and others * All rights reserved. This program and the accompanying materials * are made available under the terms of the Apache License, Version 2.0 which * accompanies this distribution and is available at * http://www.apache.org/licenses/LICENSE-2.0.txt ******************************************************************************/ package org.locationtech.spatial4j.io; import org.locationtech.spatial4j.context.SpatialContext; import org.locationtech.spatial4j.context.SpatialContextFactory; import org.locationtech.spatial4j.distance.DistanceUtils; import org.locationtech.spatial4j.exception.InvalidShapeException; import org.locationtech.spatial4j.shape.Circle; import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Shape; import org.locationtech.spatial4j.shape.ShapeFactory; import org.noggit.JSONParser; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.text.ParseException; import java.util.ArrayList; import java.util.List; public class GeoJSONReader implements ShapeReader { protected static final String BUFFER = "buffer"; protected static final String BUFFER_UNITS = "buffer_units"; protected final SpatialContext ctx; protected final ShapeFactory shapeFactory; public GeoJSONReader(SpatialContext ctx, SpatialContextFactory factory) { this.ctx = ctx; this.shapeFactory = ctx.getShapeFactory(); } @Override public String getFormatName() { return ShapeIO.GeoJSON; } @Override public final Shape read(Reader reader) throws IOException, ParseException { return readShape(new JSONParser(reader)); } @Override public Shape read(Object value) throws IOException, ParseException, InvalidShapeException { String v = value.toString().trim(); return read(new StringReader(v)); } @Override public Shape readIfSupported(Object value) throws InvalidShapeException { String v = value.toString().trim(); if (!(v.startsWith("{") && v.endsWith("}"))) { return null; } try { return read(new StringReader(v)); } catch (IOException ex) { } catch (ParseException e) { } return null; } // -------------------------------------------------------------- // Read GeoJSON // -------------------------------------------------------------- protected void readCoordXYZ(JSONParser parser, ShapeFactory.PointsBuilder pointsBuilder) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); double x = Double.NaN, y = Double.NaN, z = Double.NaN; int idx = 0; int evt = parser.nextEvent(); while (evt != JSONParser.EOF) { switch (evt) { case JSONParser.LONG: case JSONParser.NUMBER: case JSONParser.BIGNUMBER: double value = parser.getDouble(); switch(idx) { case 0: x = value; break; case 1: y = value; break; case 2: z = value; break; } idx++; break; case JSONParser.ARRAY_END: if (idx <= 2) { // don't have a 'z' pointsBuilder.pointXY(shapeFactory.normX(x), shapeFactory.normY(y)); } else { pointsBuilder.pointXYZ(shapeFactory.normX(x), shapeFactory.normY(y), shapeFactory.normZ(z)); } return; case JSONParser.STRING: case JSONParser.BOOLEAN: case JSONParser.NULL: case JSONParser.OBJECT_START: case JSONParser.OBJECT_END: case JSONParser.ARRAY_START: default: throw new ParseException("Unexpected " + JSONParser.getEventString(evt), (int) parser.getPosition()); } evt = parser.nextEvent(); } return; } protected void readCoordListXYZ(JSONParser parser, ShapeFactory.PointsBuilder pointsBuilder) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); int evt = parser.nextEvent(); while (evt != JSONParser.EOF) { switch (evt) { case JSONParser.ARRAY_START: readCoordXYZ(parser, pointsBuilder); // reads until ARRAY_END break; case JSONParser.ARRAY_END: return; default: throw new ParseException("Unexpected " + JSONParser.getEventString(evt), (int) parser.getPosition()); } evt = parser.nextEvent(); } } protected void readUntilEvent(JSONParser parser, final int event) throws IOException { int evt = parser.lastEvent(); while (true) { if (evt == event || evt == JSONParser.EOF) { return; } evt = parser.nextEvent(); } } protected Shape readPoint(JSONParser parser) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); OnePointsBuilder onePointsBuilder = new OnePointsBuilder(shapeFactory); readCoordXYZ(parser, onePointsBuilder); Point point = onePointsBuilder.getPoint(); readUntilEvent(parser, JSONParser.OBJECT_END); return point; } protected Shape readLineString(JSONParser parser) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); ShapeFactory.LineStringBuilder builder = shapeFactory.lineString(); readCoordListXYZ(parser, builder); // check for buffer field builder.buffer(readDistance(BUFFER, BUFFER_UNITS, parser)); Shape out = builder.build(); readUntilEvent(parser, JSONParser.OBJECT_END); return out; } protected Circle readCircle(JSONParser parser) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); OnePointsBuilder onePointsBuilder = new OnePointsBuilder(shapeFactory); readCoordXYZ(parser, onePointsBuilder); Point point = onePointsBuilder.getPoint(); return shapeFactory.circle(point, readDistance("radius", "radius_units", parser)); } /** * Helper method to read a up until a distance value (radius, buffer) and it's corresponding unit are found. * <p> * This method returns 0 if no distance value is found. This method currently only handles distance units of "km". * </p> * @param distProperty The name of the property containing the distance value. * @param distUnitsProperty The name of the property containing the distance unit. */ protected double readDistance(String distProperty, String distUnitsProperty, JSONParser parser) throws IOException { double dist = 0; String key = null; int event = JSONParser.OBJECT_END; int evt = parser.lastEvent(); while (true) { if (evt == event || evt == JSONParser.EOF) { break; } evt = parser.nextEvent(); if(parser.wasKey()) { key = parser.getString(); } else if(evt==JSONParser.NUMBER || evt==JSONParser.LONG) { if(distProperty.equals(key)) { dist = parser.getDouble(); } } else if(evt==JSONParser.STRING) { if(distUnitsProperty.equals(key)) { String units = parser.getString(); //TODO: support for more units? if("km".equals(units)) { // Convert KM to degrees dist = DistanceUtils.dist2Degrees(dist, DistanceUtils.EARTH_MEAN_RADIUS_KM); } } } } return shapeFactory.normDist(dist); } protected Shape readShape(JSONParser parser) throws IOException, ParseException { String type = null; String key = null; int evt = parser.nextEvent(); while (evt != JSONParser.EOF) { switch (evt) { case JSONParser.STRING: if (parser.wasKey()) { key = parser.getString(); } else { if ("type".equals(key)) { type = parser.getString(); } else { throw new ParseException("Unexpected String Value for key: " + key, (int) parser.getPosition()); } } break; case JSONParser.ARRAY_START: if ("coordinates".equals(key)) { Shape shape = readShapeFromCoordinates(type, parser); readUntilEvent(parser, JSONParser.OBJECT_END); return shape; } else if ("geometries".equals(key)) { List<Shape> shapes = new ArrayList<Shape>(); int sub = parser.nextEvent(); while (sub != JSONParser.EOF) { if (sub == JSONParser.OBJECT_START) { Shape s = readShape(parser); if (s != null) { shapes.add(s); } } else if (sub == JSONParser.OBJECT_END) { break; } sub = parser.nextEvent(); } if (shapes.isEmpty()) { throw new ParseException("Shape Collection with no geometries!", (int) parser.getPosition()); } return ctx.makeCollection(shapes); } else { throw new ParseException("Unknown type: "+type, (int) parser.getPosition()); } case JSONParser.ARRAY_END: break; case JSONParser.OBJECT_START: if (key != null) { // System.out.println("Unexpected object: " + key); } break; case JSONParser.LONG: case JSONParser.NUMBER: case JSONParser.BIGNUMBER: case JSONParser.BOOLEAN: case JSONParser.NULL: case JSONParser.OBJECT_END: // System.out.println(">>>>>" + JSONParser.getEventString(evt) + " :: " + key); break; default: throw new ParseException("Unexpected " + JSONParser.getEventString(evt), (int) parser.getPosition()); } evt = parser.nextEvent(); } throw new RuntimeException("unable to parse shape"); } protected Shape readShapeFromCoordinates(String type, JSONParser parser) throws IOException, ParseException { switch(type) { case "Point": return readPoint(parser); case "LineString": return readLineString(parser); case "Circle": return readCircle(parser); case "Polygon": return readPolygon(parser, shapeFactory.polygon()).buildOrRect(); case "MultiPoint": return readMultiPoint(parser); case "MultiLineString": return readMultiLineString(parser); case "MultiPolygon": return readMultiPolygon(parser); default: throw new ParseException("Unable to make shape type: " + type, (int) parser.getPosition()); } } protected ShapeFactory.PolygonBuilder readPolygon(JSONParser parser, ShapeFactory.PolygonBuilder polygonBuilder) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); boolean firstRing = true; int evt = parser.nextEvent(); while (true) { switch (evt) { case JSONParser.ARRAY_START: if (firstRing) { readCoordListXYZ(parser, polygonBuilder); firstRing = false; } else { ShapeFactory.PolygonBuilder.HoleBuilder holeBuilder = polygonBuilder.hole(); readCoordListXYZ(parser, holeBuilder); holeBuilder.endHole(); } break; case JSONParser.ARRAY_END: return polygonBuilder; default: throw new ParseException("Unexpected " + JSONParser.getEventString(evt), (int) parser.getPosition()); } evt = parser.nextEvent(); } } protected Shape readMultiPoint(JSONParser parser) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); ShapeFactory.MultiPointBuilder builder = shapeFactory.multiPoint(); readCoordListXYZ(parser, builder); return builder.build(); } protected Shape readMultiLineString(JSONParser parser) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); // TODO need Spatial4j LineString interface ShapeFactory.MultiLineStringBuilder builder = shapeFactory.multiLineString(); int evt = parser.nextEvent(); while (true) { switch (evt) { case JSONParser.ARRAY_START: ShapeFactory.LineStringBuilder lineStringBuilder = builder.lineString(); readCoordListXYZ(parser, lineStringBuilder); builder.add(lineStringBuilder); break; case JSONParser.ARRAY_END: return builder.build(); default: throw new ParseException("Unexpected " + JSONParser.getEventString(evt), (int) parser.getPosition()); } evt = parser.nextEvent(); } } protected Shape readMultiPolygon(JSONParser parser) throws IOException, ParseException { assert (parser.lastEvent() == JSONParser.ARRAY_START); // TODO need Spatial4j Polygon interface ShapeFactory.MultiPolygonBuilder builder = shapeFactory.multiPolygon(); int evt = parser.nextEvent(); while (true) { switch (evt) { case JSONParser.ARRAY_START: ShapeFactory.PolygonBuilder polygonBuilder = readPolygon(parser, builder.polygon()); builder.add(polygonBuilder); break; case JSONParser.ARRAY_END: return builder.build(); default: throw new ParseException("Unexpected " + JSONParser.getEventString(evt), (int) parser.getPosition()); } evt = parser.nextEvent(); } } }