From 94377f6769f14d2225427c73c99bb714e9e5c32a Mon Sep 17 00:00:00 2001 From: Ian Turton Date: Fri, 3 Jul 2020 16:47:41 +0100 Subject: [PATCH] [GEOT-6243] Issue with reading GeoJSON feature with regular geometry attribute when geometry comes after properties (#3033) * handle Geometries as properties of the GeoJSON * add static parseFilter method --- .gitignore | 3 +- .../geotools/data/geojson/GeoJSONReader.java | 62 ++++++++--- .../data/geojson/GeoJSONReaderTest.java | 102 ++++++++++++++++++ 3 files changed, 153 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 6cc1721d703..5c50f8046be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.qix *.db .project .classpath @@ -15,4 +16,4 @@ nb-configuration.xml workspace *.db .metadata -.gradle \ No newline at end of file +.gradle diff --git a/modules/unsupported/geojsonstore/src/main/java/org/geotools/data/geojson/GeoJSONReader.java b/modules/unsupported/geojsonstore/src/main/java/org/geotools/data/geojson/GeoJSONReader.java index 5ac9f89cb0f..d1dc9a7ff5e 100644 --- a/modules/unsupported/geojsonstore/src/main/java/org/geotools/data/geojson/GeoJSONReader.java +++ b/modules/unsupported/geojsonstore/src/main/java/org/geotools/data/geojson/GeoJSONReader.java @@ -20,6 +20,7 @@ import com.bedatadriven.jackson.datatype.jts.parsers.GenericGeometryParser; import com.bedatadriven.jackson.datatype.jts.parsers.GeometryParser; import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.JsonNode; @@ -27,6 +28,7 @@ import com.fasterxml.jackson.databind.node.DoubleNode; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -44,6 +46,7 @@ import org.geotools.feature.FeatureIterator; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; +import org.geotools.geometry.jts.Geometries; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.util.logging.Logging; import org.locationtech.jts.geom.Geometry; @@ -66,40 +69,62 @@ public class GeoJSONReader implements AutoCloseable { private JsonParser parser; - private JsonFactory factory; + private static JsonFactory factory = new JsonFactory();; private SimpleFeatureType schema; SimpleFeatureTypeBuilder typeBuilder = null; private SimpleFeatureBuilder builder; + private int nextID = 0; + private String baseName = "features"; private boolean schemaChanged = false; + private GeometryFactory gFac = new GeometryFactory(); + private URL url; public GeoJSONReader(URL url) throws IOException { this.url = url; - factory = new JsonFactory(); parser = factory.createParser(url); baseName = FilenameUtils.getBaseName(url.getPath()); } + public GeoJSONReader(InputStream is) throws IOException { + parser = factory.createParser(is); + } + public boolean isConnected() { + if (url != null) { + try (InputStream inputStream = url.openStream()) { + if (inputStream != null && inputStream.available() > 0) { + return true; + } + url = new URL(url.toExternalForm()); + try (InputStream inputStream2 = url.openStream()) { + return inputStream2 != null && inputStream2.available() > 0; + } - try (InputStream inputStream = url.openStream()) { - if (inputStream != null && inputStream.available() > 0) { - return true; + } catch (IOException e) { + LOGGER.log(Level.FINE, "Failure trying to determine if connected", e); + return false; } - url = new URL(url.toExternalForm()); - try (InputStream inputStream2 = url.openStream()) { - return inputStream2 != null && inputStream2.available() > 0; + } + return true; + } + + public static SimpleFeature parseFeature(String json) throws JsonParseException, IOException { + try (JsonParser lParser = factory.createParser(new ByteArrayInputStream(json.getBytes()))) { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JtsModule()); + ObjectNode node = mapper.readTree(lParser); + try (GeoJSONReader reader = new GeoJSONReader((InputStream) null)) { + SimpleFeature feature = reader.getNextFeature(node); + return feature; } - } catch (IOException e) { - LOGGER.log(Level.FINE, "Failure trying to determine if connected", e); - return false; } } @@ -160,6 +185,8 @@ private SimpleFeature getNextFeature(ObjectNode node) throws IOException { builder = getBuilder(props); } boolean restart = true; + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JtsModule()); SimpleFeature feature = null; while (restart) { restart = false; @@ -185,13 +212,16 @@ private SimpleFeature getNextFeature(ObjectNode node) throws IOException { builder.set(n.getKey(), n.getValue().asDouble()); } else if (binding == String.class) { builder.set(n.getKey(), n.getValue().textValue()); + } else if (binding.isAssignableFrom(Geometry.class)) { + GeometryParser gParser = new GenericGeometryParser(gFac); + Geometry g = gParser.geometryFromJson(n.getValue()); + builder.set(n.getKey(), g); } else { + LOGGER.warning("Unable to parse object of type " + binding); builder.set(n.getKey(), n.getValue().toString()); } } JsonNode geom = node.get("geometry"); - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JtsModule()); GeometryParser gParser = new GenericGeometryParser(gFac); Geometry g = gParser.geometryFromJson(geom); builder.set(GEOMETRY_NAME, g); @@ -224,6 +254,12 @@ private SimpleFeatureBuilder getBuilder(JsonNode props) { typeBuilder.add(n.getKey(), Integer.class); } else if (value instanceof DoubleNode) { typeBuilder.add(n.getKey(), Double.class); + } else if (value instanceof ObjectNode) { + String type = value.get("type").asText(); + Geometries namedType = Geometries.getForName(type); + if (namedType != null) { + typeBuilder.add(n.getKey(), Geometry.class, DefaultGeographicCRS.WGS84); + } } else { typeBuilder.defaultValue(""); typeBuilder.add(n.getKey(), String.class); diff --git a/modules/unsupported/geojsonstore/src/test/java/org/geotools/data/geojson/GeoJSONReaderTest.java b/modules/unsupported/geojsonstore/src/test/java/org/geotools/data/geojson/GeoJSONReaderTest.java index 49440914303..83ce09f5e2a 100644 --- a/modules/unsupported/geojsonstore/src/test/java/org/geotools/data/geojson/GeoJSONReaderTest.java +++ b/modules/unsupported/geojsonstore/src/test/java/org/geotools/data/geojson/GeoJSONReaderTest.java @@ -17,8 +17,10 @@ package org.geotools.data.geojson; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URL; import java.util.HashMap; @@ -29,8 +31,10 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; import org.opengis.feature.Feature; import org.opengis.feature.Property; +import org.opengis.feature.simple.SimpleFeature; /** @author ian */ public class GeoJSONReaderTest { @@ -72,4 +76,102 @@ public void testGetChangingSchema() throws IOException, ParseException { } } } + + @Test + public void testReadFromInputStream() throws Exception { + String input = + "{\n" + + "\"type\": \"FeatureCollection\",\n" + + "\"features\": [\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": 46.066667, \"LON\": 11.116667, \"CITY\": \"Trento\", \"NUMBER\": 140, \"YEAR\": 2002 }, \"bbox\": [ 11.117, 46.067, 11.117, 46.067 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ 11.117, 46.067 ] } },\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": 44.9441, \"LON\": -93.0852, \"CITY\": \"St Paul\", \"NUMBER\": 125, \"YEAR\": 2003 }, \"bbox\": [ -93.085, 44.944, -93.085, 44.944 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ -93.085, 44.944 ] } },\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": 13.752222, \"LON\": 100.493889, \"CITY\": \"Bangkok\", \"NUMBER\": 150, \"YEAR\": 2004 }, \"bbox\": [ 100.494, 13.752, 100.494, 13.752 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ 100.494, 13.752 ] } },\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": 45.420833, \"LON\": -75.69, \"CITY\": \"Ottawa\", \"NUMBER\": 200, \"YEAR\": 2004 }, \"bbox\": [ -75.69, 45.421, -75.69, 45.421 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ -75.69, 45.421 ] } },\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": 44.9801, \"LON\": -93.251867, \"CITY\": \"Minneapolis\", \"NUMBER\": 350, \"YEAR\": 2005 }, \"bbox\": [ -93.252, 44.98, -93.252, 44.98 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ -93.252, 44.98 ] } },\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": 46.519833, \"LON\": 6.6335, \"CITY\": \"Lausanne\", \"NUMBER\": 560, \"YEAR\": 2006 }, \"bbox\": [ 6.633, 46.52, 6.633, 46.52 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ 6.633, 46.52 ] } },\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": 48.428611, \"LON\": -123.365556, \"CITY\": \"Victoria\", \"NUMBER\": 721, \"YEAR\": 2007 }, \"bbox\": [ -123.366, 48.429, -123.366, 48.429 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ -123.366, 48.429 ] } },\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": -33.925278, \"LON\": 18.423889, \"CITY\": \"Cape Town\", \"NUMBER\": 550, \"YEAR\": 2008 }, \"bbox\": [ 18.424, -33.925, 18.424, -33.925 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ 18.424, -33.925 ] } },\n" + + "{ \"type\": \"Feature\", \"properties\": { \"LAT\": -33.859972, \"LON\": 151.21111, \"CITY\": \"Sydney\", \"NUMBER\": 436, \"YEAR\": 2009 }, \"bbox\": [ 151.211, -33.86, 151.211, -33.86 ], \"geometry\": { \"type\": \"Point\", \"coordinates\": [ 151.211, -33.86 ] } }\n" + + "]\n" + + "}"; + + try (GeoJSONReader reader = new GeoJSONReader(new ByteArrayInputStream(input.getBytes()))) { + FeatureCollection features = reader.getFeatures(); + assertNotNull(features); + assertEquals("wrong number of features read", 9, features.size()); + } + } + + @SuppressWarnings("unchecked") + @Test + public void testFeatureCollectionWithRegularGeometryAttributeReadAndGeometryAfterProperties() + throws Exception { + String geojson1 = + "{" + + "'type': 'FeatureCollection'," + + "'features': " + + "[{" + + " 'type': 'Feature'," + + " 'id': 'feature.0'," + + " 'properties': {" + + " 'otherGeometry': {" + + " 'type': 'LineString'," + + " 'coordinates': [[1.1, 1.2], [1.3, 1.4]]" + + " }" + + " }," + + " 'geometry': {" + + " 'type': 'Point'," + + " 'coordinates': [0.1, 0.1]" + + " }" + + "}" + + "]" + + "}"; + geojson1 = geojson1.replace('\'', '"'); + + SimpleFeature f = null; + try (GeoJSONReader reader = + new GeoJSONReader(new ByteArrayInputStream(geojson1.getBytes()))) { + FeatureCollection features = reader.getFeatures(); + assertNotNull(features); + assertFalse(features.isEmpty()); + f = (SimpleFeature) DataUtilities.first(features); + } catch (IOException e) { + e.printStackTrace(); + } + assertNotNull(f); + assertEquals("features.0", f.getID()); + WKTReader wkt = new WKTReader(); + assertEquals(wkt.read("POINT (0.1 0.1)"), f.getDefaultGeometry()); + assertEquals(wkt.read("LINESTRING (1.1 1.2, 1.3 1.4)"), f.getAttribute("otherGeometry")); + } + + @Test + public void testFeatureWithRegularGeometryAttributeReadAndGeometryAfterProperties() + throws Exception { + String geojson1 = + "{" + + " 'type': 'Feature'," + + " 'id': 'feature.0'," + + " 'properties': {" + + " 'otherGeometry': {" + + " 'type': 'LineString'," + + " 'coordinates': [[1.1, 1.2], [1.3, 1.4]]" + + " }" + + " }," + + " 'geometry': {" + + " 'type': 'Point'," + + " 'coordinates': [0.1, 0.1]" + + " }" + + "}"; + geojson1 = geojson1.replace('\'', '"'); + + SimpleFeature f = GeoJSONReader.parseFeature(geojson1); + assertNotNull(f); + + assertNotNull(f); + assertEquals("features.0", f.getID()); + WKTReader wkt = new WKTReader(); + assertEquals(wkt.read("POINT (0.1 0.1)"), f.getDefaultGeometry()); + assertEquals(wkt.read("LINESTRING (1.1 1.2, 1.3 1.4)"), f.getAttribute("otherGeometry")); + } }