From 7877822780b4d4b98e117e403a4c34034807dd54 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 8 Sep 2023 09:01:34 +0200 Subject: [PATCH] Improve and clean up geo utilities. --- ...JsonHttpHandler.xml => wfsHttpHandler.xml} | 3 +- org.argeo.app.geo/bnd.bnd | 2 +- .../src/org/argeo/app/geo/GeoTools.java | 34 ++++++++++ .../src/org/argeo/app/geo/GpxUtils.java | 66 +++++++++++++++---- .../argeo/app/geo/http/WfsHttpHandler.java | 12 ++-- 5 files changed, 97 insertions(+), 20 deletions(-) rename org.argeo.app.geo/OSGI-INF/{geoJsonHttpHandler.xml => wfsHttpHandler.xml} (76%) create mode 100644 org.argeo.app.geo/src/org/argeo/app/geo/GeoTools.java diff --git a/org.argeo.app.geo/OSGI-INF/geoJsonHttpHandler.xml b/org.argeo.app.geo/OSGI-INF/wfsHttpHandler.xml similarity index 76% rename from org.argeo.app.geo/OSGI-INF/geoJsonHttpHandler.xml rename to org.argeo.app.geo/OSGI-INF/wfsHttpHandler.xml index 352595b..d5646f2 100644 --- a/org.argeo.app.geo/OSGI-INF/geoJsonHttpHandler.xml +++ b/org.argeo.app.geo/OSGI-INF/wfsHttpHandler.xml @@ -4,7 +4,6 @@ - - + diff --git a/org.argeo.app.geo/bnd.bnd b/org.argeo.app.geo/bnd.bnd index 3a5153e..37b6d31 100644 --- a/org.argeo.app.geo/bnd.bnd +++ b/org.argeo.app.geo/bnd.bnd @@ -5,4 +5,4 @@ org.geotools.xml.transform,\ * Service-Component:\ -OSGI-INF/geoJsonHttpHandler.xml,\ +OSGI-INF/wfsHttpHandler.xml,\ 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 new file mode 100644 index 0000000..166c686 --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/geo/GeoTools.java @@ -0,0 +1,34 @@ +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; + +/** + * Factories initialisation and workarounds for the GeoTools 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 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) { + log.error("Basic GeoTools initialisation failed, geographical utilities are probably not available", e); + throw 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 645b08b..15f6b89 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 @@ -18,9 +18,10 @@ import javax.xml.parsers.SAXParserFactory; import org.geotools.data.DataUtilities; import org.geotools.feature.SchemaException; import org.geotools.feature.simple.SimpleFeatureBuilder; -import org.geotools.geometry.jts.JTSFactoryFinder; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.Polygon; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; @@ -30,14 +31,30 @@ import org.xml.sax.helpers.DefaultHandler; /** Utilities around the GPX format. */ public class GpxUtils { + /** GPX as a LineString in WGS84 (feature type with only the_geom). */ + public static final SimpleFeatureType LINESTRING_FEATURE_TYPE; + /** GPX as a Polygon in WGS84 (feature type with only the_geom). */ + public static final SimpleFeatureType POLYGON_FEATURE_TYPE; - public static SimpleFeature parseGpxToPolygon(InputStream in) throws IOException { + static { try { - final SimpleFeatureType TYPE = DataUtilities.createType("Area", "the_geom:Polygon:srid=4326"); - SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE); + LINESTRING_FEATURE_TYPE = DataUtilities.createType("Area", "the_geom:LineString:srid=4326"); + POLYGON_FEATURE_TYPE = DataUtilities.createType("Area", "the_geom:Polygon:srid=4326"); + } catch (SchemaException e) { + throw new RuntimeException("Cannot create GPX Feature type", e); + } + } - GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(); - List coordinates = new ArrayList<>(); + /** + * Converts a GPX track to either a {@link Geometry} with WGS84 coordinates + * ({@link LineString} or {@link Polygon}) or a {@link SimpleFeature} (with + * {@link #LINESTRING_FEATURE_TYPE}). + */ + @SuppressWarnings("unchecked") + public static T parseGpxTrackTo(InputStream in, Class clss) throws IOException { + GeometryFactory geometryFactory = GeoTools.GEOMETRY_FACTORY; + List coordinates = new ArrayList<>(); + try { SAXParserFactory factory = SAXParserFactory.newInstance(); SAXParser saxParser = factory.newSAXParser(); @@ -49,24 +66,51 @@ public class GpxUtils { if ("trkpt".equals(qName)) { Double latitude = Double.parseDouble(attributes.getValue("lat")); Double longitude = Double.parseDouble(attributes.getValue("lon")); + // TODO elevation in 3D context Coordinate coordinate = new Coordinate(longitude, latitude); coordinates.add(coordinate); } } }); + } catch (ParserConfigurationException | SAXException e) { + throw new RuntimeException("Cannot convert GPX", e); + } + + if (LineString.class.isAssignableFrom(clss)) { + LineString lineString = geometryFactory + .createLineString(coordinates.toArray(new Coordinate[coordinates.size()])); + return (T) lineString; + } else if (Polygon.class.isAssignableFrom(clss)) { // close the line string coordinates.add(coordinates.get(0)); - Polygon polygon = geometryFactory.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()])); - featureBuilder.add(polygon); + return (T) polygon; + } + // TODO MultiPoint? MultiLine? etc. + else if (SimpleFeature.class.isAssignableFrom(clss)) { + SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(LINESTRING_FEATURE_TYPE); + LineString lineString = geometryFactory + .createLineString(coordinates.toArray(new Coordinate[coordinates.size()])); + featureBuilder.add(lineString); SimpleFeature area = featureBuilder.buildFeature(null); - return area; - } catch (ParserConfigurationException | SAXException | SchemaException e) { - throw new RuntimeException("Cannot convert GPX", e); + return (T) area; + } else { + throw new IllegalArgumentException("Unsupported format " + clss); } } + /** @deprecated Use {@link #parseGpxTrackTo(InputStream, Class)} instead. */ + @Deprecated + public static SimpleFeature parseGpxToPolygon(InputStream in) throws IOException { + SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(POLYGON_FEATURE_TYPE); + Polygon polygon = parseGpxTrackTo(in, Polygon.class); + featureBuilder.add(polygon); + SimpleFeature area = featureBuilder.buildFeature(null); + return area; + } + + /** Write ODK GepShape as a GPX file. */ public static void writeGeoShapeAsGpx(String geoShape, OutputStream out) throws IOException { Objects.requireNonNull(geoShape); Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); 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 bccf98e..b10a10f 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 @@ -19,6 +19,7 @@ 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.GpxUtils; import org.argeo.cms.http.HttpHeader; import org.argeo.cms.http.server.HttpServerUtils; @@ -35,6 +36,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.Polygon; import org.opengis.feature.GeometryAttribute; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; @@ -124,21 +126,19 @@ public class WfsHttpHandler implements HttpHandler { // SimpleFeatureType featureType = DataUtilities.simple(parsed); SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType); - GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(); DefaultFeatureCollection featureCollection = new DefaultFeatureCollection(); res.forEach((c) -> { // boolean gpx = false; Geometry the_geom = null; - Geometry the_area = 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)) { - SimpleFeature feature = GpxUtils.parseGpxToPolygon(in); - the_area = (Geometry) feature.getDefaultGeometry(); + the_area = GpxUtils.parseGpxTrackTo(in, Polygon.class); } catch (IOException e) { throw new UncheckedIOException("Cannot parse " + c, e); } @@ -149,14 +149,14 @@ public class WfsHttpHandler implements HttpHandler { double longitude = c.get(WGS84PosName.lng, Double.class).get(); Coordinate coordinate = new Coordinate(longitude, latitude); - the_geom = geometryFactory.createPoint(coordinate); + the_geom = GeoTools.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_geom); + featureBuilder.set(new NameImpl(namespace, "area"), the_area); List attrDescs = featureType.getAttributeDescriptors(); for (AttributeDescriptor attrDesc : attrDescs) { -- 2.30.2