From 57f3528fda509d840818a72ebcd38b6ab3afa435 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Wed, 13 Sep 2023 09:59:37 +0200 Subject: [PATCH] GeoGson tooling --- .../src/org/argeo/app/api}/entity.xsd | 0 .../src/org/argeo/app/api/entityFeature.xsd | 36 +++ .../argeo/app/core/SuiteContentNamespace.java | 7 +- .../src/org/argeo/app/geo/GeoJSon.java | 81 +++++ .../src/org/argeo/app/geo/GeoTools.java | 4 - .../src/org/argeo/app/geo/GpxUtils.java | 2 +- .../src/org/argeo/app/geo/JTS.java | 27 ++ .../argeo/app/geo/http/WfsHttpHandler.java | 278 +++++++++++++----- 8 files changed, 358 insertions(+), 77 deletions(-) rename {org.argeo.app.core/src/org/argeo/app/core/schemas => org.argeo.app.api/src/org/argeo/app/api}/entity.xsd (100%) create mode 100644 org.argeo.app.api/src/org/argeo/app/api/entityFeature.xsd create mode 100644 org.argeo.app.geo/src/org/argeo/app/geo/GeoJSon.java create mode 100644 org.argeo.app.geo/src/org/argeo/app/geo/JTS.java diff --git a/org.argeo.app.core/src/org/argeo/app/core/schemas/entity.xsd b/org.argeo.app.api/src/org/argeo/app/api/entity.xsd similarity index 100% rename from org.argeo.app.core/src/org/argeo/app/core/schemas/entity.xsd rename to org.argeo.app.api/src/org/argeo/app/api/entity.xsd diff --git a/org.argeo.app.api/src/org/argeo/app/api/entityFeature.xsd b/org.argeo.app.api/src/org/argeo/app/api/entityFeature.xsd new file mode 100644 index 0000000..805bd1a --- /dev/null +++ b/org.argeo.app.api/src/org/argeo/app/api/entityFeature.xsd @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.app.core/src/org/argeo/app/core/SuiteContentNamespace.java b/org.argeo.app.core/src/org/argeo/app/core/SuiteContentNamespace.java index 48c508b..6b5ab3c 100644 --- a/org.argeo.app.core/src/org/argeo/app/core/SuiteContentNamespace.java +++ b/org.argeo.app.core/src/org/argeo/app/core/SuiteContentNamespace.java @@ -10,7 +10,7 @@ public enum SuiteContentNamespace implements ContentNamespace { // // ARGEO // - ENTITY("entity", "http://www.argeo.org/ns/entity", "entity.xsd", null), + ENTITY("entity", "http://www.argeo.org/ns/entity", "/org/argeo/app/api/entity.xsd", null), // ARGEO_DBK("argeodbk", "http://www.argeo.org/ns/argeodbk", null, null), // @@ -62,7 +62,10 @@ public enum SuiteContentNamespace implements ContentNamespace { Objects.requireNonNull(namespace); this.namespace = namespace; if (resourceFileName != null) { - resource = getClass().getResource(RESOURCE_BASE + resourceFileName); + if (!resourceFileName.startsWith("/")) + resource = getClass().getResource(RESOURCE_BASE + resourceFileName); + else + resource = getClass().getResource(resourceFileName); Objects.requireNonNull(resource); } if (publicUrl != null) diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/GeoJSon.java b/org.argeo.app.geo/src/org/argeo/app/geo/GeoJSon.java new file mode 100644 index 0000000..b590814 --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/geo/GeoJSon.java @@ -0,0 +1,81 @@ +package org.argeo.app.geo; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; + +import jakarta.json.stream.JsonGenerator; + +/** + * GeoJSon format. + * + * @see https://datatracker.ietf.org/doc/html/rfc7946 + */ +public class GeoJSon { + public static void writeBBox(JsonGenerator generator, Geometry geometry) { + generator.writeStartArray("bbox"); + Envelope envelope = geometry.getEnvelopeInternal(); + generator.write(envelope.getMinX()); + generator.write(envelope.getMinY()); + generator.write(envelope.getMaxX()); + generator.write(envelope.getMaxY()); + generator.writeEnd(); + } + + public static void writeGeometry(JsonGenerator generator, Geometry geometry) { + generator.writeStartObject("geometry"); + if (geometry instanceof Point point) { + generator.write("type", "Point"); + generator.writeStartArray("coordinates"); + writeCoordinate(generator, point.getCoordinate()); + generator.writeEnd();// coordinates array + } else if (geometry instanceof LineString lineString) { + generator.write("type", "LineString"); + generator.writeStartArray("coordinates"); + writeCoordinates(generator, lineString.getCoordinates()); + generator.writeEnd();// coordinates array + } else if (geometry instanceof Polygon polygon) { + generator.write("type", "Polygon"); + generator.writeStartArray("coordinates"); + LinearRing exteriorRing = polygon.getExteriorRing(); + generator.writeStartArray(); + writeCoordinates(generator, exteriorRing.getCoordinates()); + generator.writeEnd(); + for (int i = 0; i < polygon.getNumInteriorRing(); i++) { + LinearRing interiorRing = polygon.getInteriorRingN(i); + // TODO verify that holes are clockwise + generator.writeStartArray(); + writeCoordinates(generator, interiorRing.getCoordinates()); + generator.writeEnd(); + } + generator.writeEnd();// coordinates array + } + generator.writeEnd();// geometry object + } + + public static void writeCoordinates(JsonGenerator generator, Coordinate[] coordinates) { + for (Coordinate coordinate : coordinates) { + generator.writeStartArray(); + writeCoordinate(generator, coordinate); + generator.writeEnd(); + } + } + + public static void writeCoordinate(JsonGenerator generator, Coordinate coordinate) { + generator.write(coordinate.getX()); + generator.write(coordinate.getY()); + double z = coordinate.getZ(); + if (!Double.isNaN(z)) { + generator.write(z); + } + } + + /** singleton */ + private GeoJSon() { + } + +} diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/GeoTools.java b/org.argeo.app.geo/src/org/argeo/app/geo/GeoTools.java index 166c686..dbaa985 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/GeoTools.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/GeoTools.java @@ -2,9 +2,7 @@ package org.argeo.app.geo; import org.argeo.api.cms.CmsLog; import org.geotools.factory.CommonFactoryFinder; -import org.geotools.geometry.jts.JTSFactoryFinder; import org.geotools.styling.StyleFactory; -import org.locationtech.jts.geom.GeometryFactory; import org.opengis.filter.FilterFactory2; /** @@ -16,13 +14,11 @@ import org.opengis.filter.FilterFactory2; public class GeoTools { private final static CmsLog log = CmsLog.getLog(GeoTools.class); - public final static GeometryFactory GEOMETRY_FACTORY; public final static StyleFactory STYLE_FACTORY; public final static FilterFactory2 FILTER_FACTORY; static { try { - GEOMETRY_FACTORY = JTSFactoryFinder.getGeometryFactory(); STYLE_FACTORY = CommonFactoryFinder.getStyleFactory(); FILTER_FACTORY = CommonFactoryFinder.getFilterFactory2(); } catch (RuntimeException e) { diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/GpxUtils.java b/org.argeo.app.geo/src/org/argeo/app/geo/GpxUtils.java index 15f6b89..2bcff15 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/GpxUtils.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/GpxUtils.java @@ -52,7 +52,7 @@ public class GpxUtils { */ @SuppressWarnings("unchecked") public static T parseGpxTrackTo(InputStream in, Class clss) throws IOException { - GeometryFactory geometryFactory = GeoTools.GEOMETRY_FACTORY; + GeometryFactory geometryFactory = JTS.GEOMETRY_FACTORY; List coordinates = new ArrayList<>(); try { SAXParserFactory factory = SAXParserFactory.newInstance(); diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/JTS.java b/org.argeo.app.geo/src/org/argeo/app/geo/JTS.java new file mode 100644 index 0000000..2623712 --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/geo/JTS.java @@ -0,0 +1,27 @@ +package org.argeo.app.geo; + +import org.argeo.api.cms.CmsLog; +import org.locationtech.jts.geom.GeometryFactory; + +/** + * Factories initialisation and workarounds for the JTS library. The idea is to + * code defensively around factory initialisation, API changes, and issues + * related to running in an OSGi environment. Rather see {@link GeoUtils} for + * functional static utilities. + */ +public class JTS { + private final static CmsLog log = CmsLog.getLog(JTS.class); + + public final static GeometryFactory GEOMETRY_FACTORY; + + static { + try { + // GEOMETRY_FACTORY = JTSFactoryFinder.getGeometryFactory(); + GEOMETRY_FACTORY = new GeometryFactory(); + } catch (RuntimeException e) { + log.error("Basic JTS initialisation failed, geographical utilities are probably not available", e); + throw e; + } + } + +} diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java b/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java index b10a10f..d60d51d 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java @@ -1,26 +1,33 @@ package org.argeo.app.geo.http; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URL; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentSession; +import org.argeo.api.acr.DName; import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.QNamed; import org.argeo.api.acr.ldap.LdapAttr; import org.argeo.api.acr.spi.ProvidedRepository; +import org.argeo.api.cms.CmsLog; import org.argeo.app.api.EntityName; import org.argeo.app.api.EntityType; import org.argeo.app.api.WGS84PosName; import org.argeo.app.geo.CqlUtils; -import org.argeo.app.geo.GeoTools; +import org.argeo.app.geo.GeoJSon; import org.argeo.app.geo.GpxUtils; +import org.argeo.app.geo.JTS; import org.argeo.cms.http.HttpHeader; import org.argeo.cms.http.server.HttpServerUtils; import org.geotools.data.DataUtilities; @@ -36,6 +43,7 @@ import org.geotools.wfs.GML.Version; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.opengis.feature.GeometryAttribute; import org.opengis.feature.simple.SimpleFeature; @@ -46,8 +54,12 @@ import org.opengis.feature.type.Name; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; +import jakarta.json.Json; +import jakarta.json.stream.JsonGenerator; + /** A partially implemented WFS 2.0 server. */ public class WfsHttpHandler implements HttpHandler { + private final static CmsLog log = CmsLog.getLog(WfsHttpHandler.class); private ProvidedRepository contentRepository; // HTTP parameters @@ -107,88 +119,139 @@ public class WfsHttpHandler implements HttpHandler { exchange.sendResponseHeaders(200, 0); - if ("GML3".equals(outputFormat)) { - String entityType = "apafField"; - URL schemaLocation = new URL("http://localhost:7070/pkg/eu.netiket.on.apaf/apaf.xsd"); - String namespace = "http://apaf.on.netiket.eu/ns/apaf"; - - GML gml = new GML(Version.WFS1_1); - gml.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84); - gml.setNamespace("local", namespace); + final int BUFFER_SIZE = 100 * 1024; + try (BufferedOutputStream out = new BufferedOutputStream(exchange.getResponseBody(), BUFFER_SIZE)) { + if ("GML3".equals(outputFormat)) { + encodeCollectionAsGML(res, out); + } else if ("application/json".equals(outputFormat)) { + encodeCollectionAsGeoJSon(res, out); + } + } + } - SimpleFeatureType featureType = gml.decodeSimpleFeatureType(schemaLocation, - new NameImpl(namespace, entityType + "Feature")); + protected void encodeCollectionAsGeoJSon(Stream features, OutputStream out) throws IOException { + long begin = System.currentTimeMillis(); + AtomicLong count = new AtomicLong(0); + JsonGenerator generator = Json.createGenerator(out); + generator.writeStartObject(); + generator.write("type", "FeatureCollection"); + generator.writeStartArray("features"); + features.forEach((c) -> { + Geometry defaultGeometry = getDefaultGeometry(c); + if (defaultGeometry == null) + return; + generator.writeStartObject(); + generator.write("type", "Feature"); + String featureId = getFeatureId(c); + if (featureId != null) + generator.write("id", featureId); + GeoJSon.writeBBox(generator, defaultGeometry); + GeoJSon.writeGeometry(generator, defaultGeometry); + + generator.writeStartObject("properties"); + writeTimeProperties(generator, c); + writeProperties(generator, c); + generator.writeEnd();// properties object + + generator.writeEnd();// feature object + + if (count.incrementAndGet() % 10 == 0) + try { + out.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + generator.writeEnd();// features array + generator.writeEnd().close(); -// CoordinateReferenceSystem crs=DefaultGeographicCRS.WGS84; -// QName featureName = new QName(namespace,"apafFieldFeature"); -// GMLConfiguration configuration = new GMLConfiguration(); -// FeatureType parsed = GTXML.parseFeatureType(configuration, featureName, crs); -// SimpleFeatureType featureType = DataUtilities.simple(parsed); + log.debug("GeoJSon encoding took " + (System.currentTimeMillis() - begin) + " ms."); + } - SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType); + protected Geometry getDefaultGeometry(Content content) { + if (content.hasContentClass(EntityType.geopoint)) { + double latitude = content.get(WGS84PosName.lat, Double.class).get(); + double longitude = content.get(WGS84PosName.lng, Double.class).get(); - DefaultFeatureCollection featureCollection = new DefaultFeatureCollection(); + Coordinate coordinate = new Coordinate(longitude, latitude); + Point the_geom = JTS.GEOMETRY_FACTORY.createPoint(coordinate); + return the_geom; + } + return null; + } - res.forEach((c) -> { -// boolean gpx = false; - Geometry the_geom = null; - Polygon the_area = null; -// if (gpx) { - Content area = c.getContent("gpx/area.gpx").orElse(null); - if (area != null) { + protected String getFeatureId(Content content) { + String uuid = content.attr(LdapAttr.entryUUID); + return uuid; + } - try (InputStream in = area.open(InputStream.class)) { - the_area = GpxUtils.parseGpxTrackTo(in, Polygon.class); - } catch (IOException e) { - throw new UncheckedIOException("Cannot parse " + c, e); - } - } -// } else { - if (c.hasContentClass(EntityType.geopoint)) { - double latitude = c.get(WGS84PosName.lat, Double.class).get(); - double longitude = c.get(WGS84PosName.lng, Double.class).get(); + private final QName JCR_CREATED = NamespaceUtils.parsePrefixedName("jcr:created"); + + private final QName JCR_LAST_MODIFIED = NamespaceUtils.parsePrefixedName("jcr:lastModified"); + + protected void writeTimeProperties(JsonGenerator g, Content content) { + String creationDate = content.attr(DName.creationdate); + if (creationDate == null) + creationDate = content.attr(JCR_CREATED); + if (creationDate != null) + g.write(DName.creationdate.get(), creationDate); + String lastModified = content.attr(DName.getlastmodified); + if (lastModified == null) + lastModified = content.attr(JCR_LAST_MODIFIED); + if (lastModified != null) + g.write(DName.getlastmodified.get(), lastModified); + } - Coordinate coordinate = new Coordinate(longitude, latitude); - the_geom = GeoTools.GEOMETRY_FACTORY.createPoint(coordinate); - } + protected void writeProperties(JsonGenerator generator, Content content) { + String path = content.getPath(); + generator.write("path", path); + if (content.hasContentClass(EntityType.local)) { + String type = content.attr(EntityName.type); + generator.write("type", type); + } else { + List contentClasses = content.getContentClasses(); + if (!contentClasses.isEmpty()) { + generator.write("type", NamespaceUtils.toPrefixedName(contentClasses.get(0))); + } + } -// } - if (the_geom != null) - featureBuilder.set(new NameImpl(namespace, "geopoint"), the_geom); - if (the_area != null) - featureBuilder.set(new NameImpl(namespace, "area"), the_area); - - List attrDescs = featureType.getAttributeDescriptors(); - for (AttributeDescriptor attrDesc : attrDescs) { - if (attrDesc instanceof GeometryAttribute) - continue; - Name name = attrDesc.getName(); - QName qName = new QName(name.getNamespaceURI(), name.getLocalPart()); - String value = c.attr(qName); - if (value == null) { - value = c.attr(name.getLocalPart()); - } - if (value != null) { - featureBuilder.set(name, value); - } - } + } - String uuid = c.attr(LdapAttr.entryUUID); + protected void writeAttr(JsonGenerator g, Content content, String attr) { + writeAttr(g, content, NamespaceUtils.parsePrefixedName(attr)); + } - SimpleFeature feature = featureBuilder.buildFeature(uuid); - featureCollection.add(feature); + protected void writeAttr(JsonGenerator g, Content content, QNamed attr) { + writeAttr(g, content, attr.qName()); + } - }); - gml.encode(exchange.getResponseBody(), featureCollection); - exchange.getResponseBody().close(); + protected void writeAttr(JsonGenerator g, Content content, QName attr) { + // String value = content.attr(attr); + Object value = content.get(attr); + if (value != null) { + // TODO specify NamespaceContext + String key = NamespaceUtils.toPrefixedName(attr); + if (value instanceof Double v) + g.write(key, v); + else if (value instanceof Long v) + g.write(key, v); + else if (value instanceof Integer v) + g.write(key, v); + else if (value instanceof Boolean v) + g.write(key, v); + else + g.write(key, value.toString()); + } + } - } else if ("application/json".equals(outputFormat)) { + protected void encodeCollectionAsGeoJSonOld(Stream features, OutputStream out) throws IOException { - // BODY PROCESSING - GeoJSONWriter geoJSONWriter = new GeoJSONWriter(exchange.getResponseBody()); + // BODY PROCESSING + try (GeoJSONWriter geoJSONWriter = new GeoJSONWriter(out)) { geoJSONWriter.setPrettyPrinting(true); + geoJSONWriter.setEncodeFeatureBounds(true); - boolean gpx = false; + boolean gpx = true; SimpleFeatureType TYPE; try { if (gpx) @@ -203,7 +266,7 @@ public class WfsHttpHandler implements HttpHandler { SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE); GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(); - res.forEach((c) -> { + features.forEach((c) -> { Geometry the_geom; if (gpx) {// experimental Content area = c.getContent("gpx/area.gpx").orElse(null); @@ -250,8 +313,83 @@ public class WfsHttpHandler implements HttpHandler { throw new UncheckedIOException(e); } }); - geoJSONWriter.close(); } + } + + protected void encodeCollectionAsGML(Stream features, OutputStream out) throws IOException { + String entityType = "entity"; + URL schemaLocation = getClass().getResource("/org/argeo/app/api/entity.xsd"); + String namespace = "http://www.argeo.org/ns/entity"; + + GML gml = new GML(Version.WFS1_1); + gml.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84); + gml.setNamespace("local", namespace); + + SimpleFeatureType featureType = gml.decodeSimpleFeatureType(schemaLocation, + new NameImpl(namespace, entityType + "Feature")); + +// CoordinateReferenceSystem crs=DefaultGeographicCRS.WGS84; +// QName featureName = new QName(namespace,"apafFieldFeature"); +// GMLConfiguration configuration = new GMLConfiguration(); +// FeatureType parsed = GTXML.parseFeatureType(configuration, featureName, crs); +// SimpleFeatureType featureType = DataUtilities.simple(parsed); + + SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType); + + DefaultFeatureCollection featureCollection = new DefaultFeatureCollection(); + + features.forEach((c) -> { +// boolean gpx = false; + Geometry the_geom = null; + Polygon the_area = null; +// if (gpx) { + Content area = c.getContent("gpx/area.gpx").orElse(null); + if (area != null) { + + try (InputStream in = area.open(InputStream.class)) { + the_area = GpxUtils.parseGpxTrackTo(in, Polygon.class); + } catch (IOException e) { + throw new UncheckedIOException("Cannot parse " + c, e); + } + } +// } else { + if (c.hasContentClass(EntityType.geopoint)) { + double latitude = c.get(WGS84PosName.lat, Double.class).get(); + double longitude = c.get(WGS84PosName.lng, Double.class).get(); + + Coordinate coordinate = new Coordinate(longitude, latitude); + the_geom = JTS.GEOMETRY_FACTORY.createPoint(coordinate); + } + +// } + if (the_geom != null) + featureBuilder.set(new NameImpl(namespace, "geopoint"), the_geom); + if (the_area != null) + featureBuilder.set(new NameImpl(namespace, "area"), the_area); + + List attrDescs = featureType.getAttributeDescriptors(); + for (AttributeDescriptor attrDesc : attrDescs) { + if (attrDesc instanceof GeometryAttribute) + continue; + Name name = attrDesc.getName(); + QName qName = new QName(name.getNamespaceURI(), name.getLocalPart()); + String value = c.attr(qName); + if (value == null) { + value = c.attr(name.getLocalPart()); + } + if (value != null) { + featureBuilder.set(name, value); + } + } + + String uuid = c.attr(LdapAttr.entryUUID); + + SimpleFeature feature = featureBuilder.buildFeature(uuid); + featureCollection.add(feature); + + }); + gml.encode(out, featureCollection); + out.close(); } -- 2.30.2