From 07d4329086be993ff25fc6342c97b681b2e07433 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Mon, 9 May 2022 10:50:15 +0200 Subject: [PATCH] Introduce GIS utilities. --- org.argeo.app.core/OSGI-INF/geoToolsTest.xml | 4 + org.argeo.app.core/bnd.bnd | 1 + .../src/org/argeo/app/geo/GeoToolsTest.java | 221 ++++++++++++++++++ .../src/org/argeo/app/geo/GeoUtils.java | 44 ++++ .../src/org/argeo/app/geo/GpxUtils.java | 68 ++++++ .../icons/types/svg/inbox.svg | 14 ++ 6 files changed, 352 insertions(+) create mode 100644 org.argeo.app.core/OSGI-INF/geoToolsTest.xml create mode 100644 org.argeo.app.core/src/org/argeo/app/geo/GeoToolsTest.java create mode 100644 org.argeo.app.core/src/org/argeo/app/geo/GeoUtils.java create mode 100644 org.argeo.app.core/src/org/argeo/app/geo/GpxUtils.java create mode 100644 org.argeo.app.theme.default/icons/types/svg/inbox.svg diff --git a/org.argeo.app.core/OSGI-INF/geoToolsTest.xml b/org.argeo.app.core/OSGI-INF/geoToolsTest.xml new file mode 100644 index 0000000..68a53ab --- /dev/null +++ b/org.argeo.app.core/OSGI-INF/geoToolsTest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/org.argeo.app.core/bnd.bnd b/org.argeo.app.core/bnd.bnd index 8604ec9..9d8228b 100644 --- a/org.argeo.app.core/bnd.bnd +++ b/org.argeo.app.core/bnd.bnd @@ -6,6 +6,7 @@ OSGI-INF/maintenanceService.xml,\ OSGI-INF/dbk4Converter.xml,\ Import-Package:\ +tech.units.indriya.unit,\ org.osgi.service.useradmin,\ javax.jcr.nodetype,\ javax.jcr.security,\ diff --git a/org.argeo.app.core/src/org/argeo/app/geo/GeoToolsTest.java b/org.argeo.app.core/src/org/argeo/app/geo/GeoToolsTest.java new file mode 100644 index 0000000..a771196 --- /dev/null +++ b/org.argeo.app.core/src/org/argeo/app/geo/GeoToolsTest.java @@ -0,0 +1,221 @@ +package org.argeo.app.geo; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.geotools.data.DataUtilities; +import org.geotools.data.DefaultTransaction; +import org.geotools.data.Transaction; +import org.geotools.data.collection.ListFeatureCollection; +import org.geotools.data.shapefile.ShapefileDataStore; +import org.geotools.data.shapefile.ShapefileDataStoreFactory; +import org.geotools.data.simple.SimpleFeatureCollection; +import org.geotools.data.simple.SimpleFeatureSource; +import org.geotools.data.simple.SimpleFeatureStore; +import org.geotools.feature.simple.SimpleFeatureBuilder; +import org.geotools.geometry.jts.JTSFactoryFinder; +import org.geotools.swing.data.JFileDataStoreChooser; +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.Point; +import org.opengis.feature.simple.SimpleFeature; +import org.opengis.feature.simple.SimpleFeatureType; + +public class GeoToolsTest { + public GeoToolsTest() { + + } + + public void init() { + try { + main(null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void destroy() { + + } + + public static void main(String[] args) throws Exception { + final SimpleFeatureType TYPE = DataUtilities.createType("Location", "the_geom:Point:srid=4326," + // <- the + // geometry + // attribute: + // Point + // type + "name:String," + // <- a String attribute + "number:Integer" // a number attribute + ); + final SimpleFeatureType TYPE_HULL = DataUtilities.createType("Hull", "the_geom:MultiPolygon:srid=4326"); + System.out.println("TYPE:" + TYPE); + + /* + * A list to collect features as we create them. + */ + List features = new ArrayList<>(); + List coordinates = new ArrayList<>(); + + /* + * GeometryFactory will be used to create the geometry attribute of each + * feature, using a Point object for the location. + */ + GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(); + + SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(GeoToolsTest.class.getResourceAsStream("/org/djapps/on/apaf/locations.csv")))) { + /* First line of the data file is the header */ + String line = reader.readLine(); + System.out.println("Header: " + line); + + for (line = reader.readLine(); line != null; line = reader.readLine()) { + if (line.trim().length() > 0) { // skip blank lines + String[] tokens = line.split("\\,"); + + double latitude = Double.parseDouble(tokens[0]); + double longitude = Double.parseDouble(tokens[1]); + String name = tokens[2].trim(); + int number = Integer.parseInt(tokens[3].trim()); + + /* Longitude (= x coord) first ! */ + Coordinate coordinate = new Coordinate(longitude, latitude); + coordinates.add(coordinate); + Point point = geometryFactory.createPoint(coordinate); + + featureBuilder.add(point); + featureBuilder.add(name); + featureBuilder.add(number); + SimpleFeature feature = featureBuilder.buildFeature(null); + features.add(feature); + } + } + } + + LineString lineString = geometryFactory + .createLineString(coordinates.toArray(new Coordinate[coordinates.size()])); + Geometry convexHull = lineString.convexHull(); + System.out.println(convexHull.toText()); + SimpleFeatureBuilder hullFeatureBuilder = new SimpleFeatureBuilder(TYPE_HULL); + hullFeatureBuilder.add(convexHull); + SimpleFeature hull = hullFeatureBuilder.buildFeature(null); + + /* + * Get an output file name and create the new shapefile + */ + File newFile = getNewShapeFile(); + + ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory(); + + Map params = new HashMap<>(); + params.put("url", newFile.toURI().toURL()); + params.put("create spatial index", Boolean.TRUE); + + ShapefileDataStore newDataStore = (ShapefileDataStore) dataStoreFactory.createNewDataStore(params); + + /* + * TYPE is used as a template to describe the file contents + */ + newDataStore.createSchema(TYPE_HULL); + + /* + * Write the features to the shapefile + */ + Transaction transaction = new DefaultTransaction("create"); + + String typeName = newDataStore.getTypeNames()[0]; + SimpleFeatureSource featureSource = newDataStore.getFeatureSource(typeName); + SimpleFeatureType SHAPE_TYPE = featureSource.getSchema(); + /* + * The Shapefile format has a couple limitations: - "the_geom" is always first, + * and used for the geometry attribute name - "the_geom" must be of type Point, + * MultiPoint, MuiltiLineString, MultiPolygon - Attribute names are limited in + * length - Not all data types are supported (example Timestamp represented as + * Date) + * + * Each data store has different limitations so check the resulting + * SimpleFeatureType. + */ + System.out.println("SHAPE:" + SHAPE_TYPE); + + if (featureSource instanceof SimpleFeatureStore) { + SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; + /* + * SimpleFeatureStore has a method to add features from a + * SimpleFeatureCollection object, so we use the ListFeatureCollection class to + * wrap our list of features. + */ + SimpleFeatureCollection collection = new ListFeatureCollection(TYPE, Collections.singletonList(hull)); + featureStore.setTransaction(transaction); + try { + featureStore.addFeatures(collection); + transaction.commit(); + } catch (Exception problem) { + problem.printStackTrace(); + transaction.rollback(); + } finally { + transaction.close(); + } + } else { + System.out.println(typeName + " does not support read/write access"); + } +// if (featureSource instanceof SimpleFeatureStore) { +// SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; +// /* +// * SimpleFeatureStore has a method to add features from a +// * SimpleFeatureCollection object, so we use the ListFeatureCollection class to +// * wrap our list of features. +// */ +// SimpleFeatureCollection collection = new ListFeatureCollection(TYPE, features); +// featureStore.setTransaction(transaction); +// try { +// featureStore.addFeatures(collection); +// transaction.commit(); +// } catch (Exception problem) { +// problem.printStackTrace(); +// transaction.rollback(); +// } finally { +// transaction.close(); +// } +// } else { +// System.out.println(typeName + " does not support read/write access"); +// } + } + + /** + * Prompt the user for the name and path to use for the output shapefile + * + * @param csvFile the input csv file used to create a default shapefile name + * @return name and path for the shapefile as a new File object + */ + private static File getNewShapeFile() { +// String path = csvFile.getAbsolutePath(); +// String newPath = path.substring(0, path.length() - 4) + ".shp"; + + JFileDataStoreChooser chooser = new JFileDataStoreChooser("shp"); + chooser.setDialogTitle("Save shapefile"); +// chooser.setSelectedFile(new File(newPath)); + + int returnVal = chooser.showSaveDialog(null); + + if (returnVal != JFileDataStoreChooser.APPROVE_OPTION) { + // the user cancelled the dialog + System.exit(0); + } + + File newFile = chooser.getSelectedFile(); + + return newFile; + } + +} diff --git a/org.argeo.app.core/src/org/argeo/app/geo/GeoUtils.java b/org.argeo.app.core/src/org/argeo/app/geo/GeoUtils.java new file mode 100644 index 0000000..8da7c4e --- /dev/null +++ b/org.argeo.app.core/src/org/argeo/app/geo/GeoUtils.java @@ -0,0 +1,44 @@ +package org.argeo.app.geo; + +import javax.measure.Quantity; +import javax.measure.quantity.Area; + +import org.geotools.geometry.jts.JTS; +import org.geotools.referencing.CRS; +import org.geotools.referencing.crs.DefaultGeographicCRS; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.opengis.feature.simple.SimpleFeature; +import org.opengis.geometry.MismatchedDimensionException; +import org.opengis.referencing.FactoryException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.TransformException; + +import si.uom.SI; +import tech.units.indriya.quantity.Quantities; + +/** Utilities around geographical format, mostly wrapping GeoTools patterns. */ +public class GeoUtils { + + /** In square meters. */ + public static Quantity calcArea(SimpleFeature feature) { + try { + Polygon p = (Polygon) feature.getDefaultGeometry(); + Point centroid = p.getCentroid(); + String code = "AUTO:42001," + centroid.getX() + "," + centroid.getY(); + CoordinateReferenceSystem auto = CRS.decode(code); + + MathTransform transform = CRS.findMathTransform(DefaultGeographicCRS.WGS84, auto); + + Polygon projed = (Polygon) JTS.transform(p, transform); + return Quantities.getQuantity(projed.getArea(), SI.SQUARE_METRE); + } catch (MismatchedDimensionException | FactoryException | TransformException e) { + throw new IllegalStateException("Cannot claculate area of feature"); + } + } + + /** Singleton. */ + private GeoUtils() { + } +} diff --git a/org.argeo.app.core/src/org/argeo/app/geo/GpxUtils.java b/org.argeo.app.core/src/org/argeo/app/geo/GpxUtils.java new file mode 100644 index 0000000..be028d3 --- /dev/null +++ b/org.argeo.app.core/src/org/argeo/app/geo/GpxUtils.java @@ -0,0 +1,68 @@ +package org.argeo.app.geo; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +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.GeometryFactory; +import org.locationtech.jts.geom.Polygon; +import org.opengis.feature.simple.SimpleFeature; +import org.opengis.feature.simple.SimpleFeatureType; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** Utilities around the GPX format. */ +public class GpxUtils { + + public static SimpleFeature parseGpxToPolygon(InputStream in) { + try { + final SimpleFeatureType TYPE = DataUtilities.createType("Area", "the_geom:Polygon:srid=4326"); + SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE); + + GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(); + List coordinates = new ArrayList<>(); + SAXParserFactory factory = SAXParserFactory.newInstance(); + Double[] startCoord = new Double[2]; + SAXParser saxParser = factory.newSAXParser(); + + saxParser.parse(in, new DefaultHandler() { + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + if ("trkpt".equals(qName)) { + Double latitude = Double.parseDouble(attributes.getValue("lat")); + Double longitude = Double.parseDouble(attributes.getValue("lon")); + Coordinate coordinate = new Coordinate(longitude, latitude); + coordinates.add(coordinate); + } + } + + }); + // close the line string + coordinates.add(coordinates.get(0)); + + Polygon polygon = geometryFactory.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()])); + featureBuilder.add(polygon); + SimpleFeature area = featureBuilder.buildFeature(null); + return area; + } catch (ParserConfigurationException | SAXException | IOException | SchemaException e) { + throw new RuntimeException("Cannot convert GPX", e); + } + } + + /** Singleton. */ + private GpxUtils() { + } +} diff --git a/org.argeo.app.theme.default/icons/types/svg/inbox.svg b/org.argeo.app.theme.default/icons/types/svg/inbox.svg new file mode 100644 index 0000000..d1a171c --- /dev/null +++ b/org.argeo.app.theme.default/icons/types/svg/inbox.svg @@ -0,0 +1,14 @@ + + + + + ascending + + + + + + + + + -- 2.30.2