+++ /dev/null
-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.JsonArray;
-import jakarta.json.JsonObject;
-import jakarta.json.stream.JsonGenerator;
-
-/**
- * GeoJSon format.
- *
- * @see https://datatracker.ietf.org/doc/html/rfc7946
- */
-public class GeoJSon {
- public final static String POINT_TYPE = "Point";
- public final static String LINE_STRING_TYPE = "LineString";
- public final static String POLYGON_TYPE = "Polygon";
-
- public final static String TYPE = "type";
- public final static String GEOMETRY = "geometry";
- public final static String COORDINATES = "coordinates";
- public final static String BBOX = "bbox";
- public final static String PROPERTIES = "properties";
-
- /*
- * WRITE
- */
- /** Writes a {@link Geometry} as GeoJSON. */
- public static void writeGeometry(JsonGenerator generator, Geometry geometry) {
- if (geometry instanceof Point point) {
- generator.write(TYPE, POINT_TYPE);
- generator.writeStartArray(COORDINATES);
- writeCoordinate(generator, point.getCoordinate());
- generator.writeEnd();// coordinates array
- } else if (geometry instanceof LineString lineString) {
- generator.write(TYPE, LINE_STRING_TYPE);
- generator.writeStartArray(COORDINATES);
- writeCoordinates(generator, lineString.getCoordinates());
- generator.writeEnd();// coordinates array
- } else if (geometry instanceof Polygon polygon) {
- generator.write(TYPE, POLYGON_TYPE);
- 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
- }
- }
-
- /** Writes a sequence of coordinates [[lat,lon],[lat,lon]] */
- public static void writeCoordinates(JsonGenerator generator, Coordinate[] coordinates) {
- for (Coordinate coordinate : coordinates) {
- generator.writeStartArray();
- writeCoordinate(generator, coordinate);
- generator.writeEnd();
- }
- }
-
- /** Writes a pair of coordinates [lat,lon]. */
- 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);
- }
- }
-
- /**
- * Writes the {@link Envelope} of a {@link Geometry} as a bbox GeoJSON object.
- */
- 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();
- }
-
- /*
- * READ
- */
- /** Reads a geometry from the geometry object of a GEoJSON feature. */
- @SuppressWarnings("unchecked")
- public static <T extends Geometry> T readGeometry(JsonObject geom, Class<T> clss) {
- String type = geom.getString(TYPE);
- JsonArray coordinates = geom.getJsonArray(COORDINATES);
- Geometry res = switch (type) {
- case POINT_TYPE: {
- Coordinate coord = readCoordinate(coordinates);
- yield JTS.GEOMETRY_FACTORY_WGS84.createPoint(coord);
- }
- case LINE_STRING_TYPE: {
- Coordinate[] coords = readCoordinates(coordinates);
- yield JTS.GEOMETRY_FACTORY_WGS84.createLineString(coords);
- }
- case POLYGON_TYPE: {
- assert coordinates.size() > 0;
- LinearRing exterior = JTS.GEOMETRY_FACTORY_WGS84
- .createLinearRing(readCoordinates(coordinates.getJsonArray(0)));
- LinearRing[] holes = new LinearRing[coordinates.size() - 1];
- for (int i = 0; i < coordinates.size() - 1; i++) {
- holes[i] = JTS.GEOMETRY_FACTORY_WGS84
- .createLinearRing(readCoordinates(coordinates.getJsonArray(i + 1)));
- }
- yield JTS.GEOMETRY_FACTORY_WGS84.createPolygon(exterior, holes);
- }
- default:
- throw new IllegalArgumentException("Unexpected value: " + type);
- };
-// res.normalize();
- return (T)res;
- }
-
- /** Reads a coordinate sequence [[lat,lon],[lat,lon]]. */
- public static Coordinate readCoordinate(JsonArray arr) {
- assert arr.size() >= 2;
- return new Coordinate(arr.getJsonNumber(0).doubleValue(), arr.getJsonNumber(1).doubleValue());
- }
-
- /** Reads a coordinate pair [lat,lon]. */
- public static Coordinate[] readCoordinates(JsonArray arr) {
- Coordinate[] coords = new Coordinate[arr.size()];
- for (int i = 0; i < arr.size(); i++)
- coords[i] = readCoordinate(arr.getJsonArray(i));
- return coords;
- }
-
- /** singleton */
- private GeoJSon() {
- }
-
-}
--- /dev/null
+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.GeometryCollection;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.MultiPoint;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.json.stream.JsonGenerator;
+
+/**
+ * GeoJSon format.
+ *
+ * @see https://datatracker.ietf.org/doc/html/rfc7946
+ */
+public class GeoJson {
+ public final static String POINT_TYPE = "Point";
+ public final static String MULTI_POINT_TYPE = "MultiPoint";
+ public final static String LINE_STRING_TYPE = "LineString";
+ public final static String POLYGON_TYPE = "Polygon";
+ public final static String GEOMETRY_COLLECTION_TYPE = "GeometryCollection";
+
+ public final static String TYPE = "type";
+ public final static String GEOMETRY = "geometry";
+ public final static String GEOMETRIES = "geometries";
+ public final static String COORDINATES = "coordinates";
+ public final static String BBOX = "bbox";
+ public final static String PROPERTIES = "properties";
+
+ /*
+ * WRITE
+ */
+ /** Writes a {@link Geometry} as GeoJSON. */
+ public static void writeGeometry(JsonGenerator g, Geometry geometry) {
+ if (geometry instanceof Point point) {
+ g.write(TYPE, POINT_TYPE);
+ g.writeStartArray(COORDINATES);
+ writeCoordinate(g, point.getCoordinate());
+ g.writeEnd();// coordinates array
+ } else if (geometry instanceof MultiPoint multiPoint) {
+ g.write(TYPE, MULTI_POINT_TYPE);
+ g.writeStartArray(COORDINATES);
+ writeCoordinates(g, multiPoint.getCoordinates());
+ g.writeEnd();// coordinates array
+ } else if (geometry instanceof LineString lineString) {
+ g.write(TYPE, LINE_STRING_TYPE);
+ g.writeStartArray(COORDINATES);
+ writeCoordinates(g, lineString.getCoordinates());
+ g.writeEnd();// coordinates array
+ } else if (geometry instanceof Polygon polygon) {
+ g.write(TYPE, POLYGON_TYPE);
+ g.writeStartArray(COORDINATES);
+ LinearRing exteriorRing = polygon.getExteriorRing();
+ g.writeStartArray();
+ writeCoordinates(g, exteriorRing.getCoordinates());
+ g.writeEnd();
+ for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
+ LinearRing interiorRing = polygon.getInteriorRingN(i);
+ // TODO verify that holes are clockwise
+ g.writeStartArray();
+ writeCoordinates(g, interiorRing.getCoordinates());
+ g.writeEnd();
+ }
+ g.writeEnd();// coordinates array
+ } else if (geometry instanceof GeometryCollection geometryCollection) {// must be last
+ g.write(TYPE, GEOMETRY_COLLECTION_TYPE);
+ g.writeStartArray(GEOMETRIES);
+ for (int i = 0; i < geometryCollection.getNumGeometries(); i++) {
+ g.writeStartObject();
+ writeGeometry(g, geometryCollection.getGeometryN(i));
+ g.writeEnd();// geometry object
+ }
+ g.writeEnd();// geometries array
+ } else {
+ throw new IllegalArgumentException(geometry.getClass() + " is not supported.");
+ }
+ }
+
+ /** Writes a sequence of coordinates [[lat,lon],[lat,lon]] */
+ public static void writeCoordinates(JsonGenerator g, Coordinate[] coordinates) {
+ for (Coordinate coordinate : coordinates) {
+ g.writeStartArray();
+ writeCoordinate(g, coordinate);
+ g.writeEnd();
+ }
+ }
+
+ /** Writes a pair of coordinates [lat,lon]. */
+ public static void writeCoordinate(JsonGenerator g, Coordinate coordinate) {
+ g.write(coordinate.getX());
+ g.write(coordinate.getY());
+ double z = coordinate.getZ();
+ if (!Double.isNaN(z)) {
+ g.write(z);
+ }
+ }
+
+ /**
+ * Writes the {@link Envelope} of a {@link Geometry} as a bbox GeoJSON object.
+ */
+ public static void writeBBox(JsonGenerator g, Geometry geometry) {
+ g.writeStartArray(BBOX);
+ Envelope envelope = geometry.getEnvelopeInternal();
+ g.write(envelope.getMinX());
+ g.write(envelope.getMinY());
+ g.write(envelope.getMaxX());
+ g.write(envelope.getMaxY());
+ g.writeEnd();
+ }
+
+ /*
+ * READ
+ */
+ /** Reads a geometry from the geometry object of a GEoJSON feature. */
+ @SuppressWarnings("unchecked")
+ public static <T extends Geometry> T readGeometry(JsonObject geom, Class<T> clss) {
+ String type = geom.getString(TYPE);
+ JsonArray coordinates = geom.getJsonArray(COORDINATES);
+ Geometry res = switch (type) {
+ case POINT_TYPE: {
+ Coordinate coord = readCoordinate(coordinates);
+ yield JTS.GEOMETRY_FACTORY_WGS84.createPoint(coord);
+ }
+ case LINE_STRING_TYPE: {
+ Coordinate[] coords = readCoordinates(coordinates);
+ yield JTS.GEOMETRY_FACTORY_WGS84.createLineString(coords);
+ }
+ case POLYGON_TYPE: {
+ assert coordinates.size() > 0;
+ LinearRing exterior = JTS.GEOMETRY_FACTORY_WGS84
+ .createLinearRing(readCoordinates(coordinates.getJsonArray(0)));
+ LinearRing[] holes = new LinearRing[coordinates.size() - 1];
+ for (int i = 0; i < coordinates.size() - 1; i++) {
+ holes[i] = JTS.GEOMETRY_FACTORY_WGS84
+ .createLinearRing(readCoordinates(coordinates.getJsonArray(i + 1)));
+ }
+ yield JTS.GEOMETRY_FACTORY_WGS84.createPolygon(exterior, holes);
+ }
+ default:
+ throw new IllegalArgumentException("Unexpected value: " + type);
+ };
+// res.normalize();
+ return (T) res;
+ }
+
+ /** Reads a coordinate sequence [[lat,lon],[lat,lon]]. */
+ public static Coordinate readCoordinate(JsonArray arr) {
+ assert arr.size() >= 2;
+ return new Coordinate(arr.getJsonNumber(0).doubleValue(), arr.getJsonNumber(1).doubleValue());
+ }
+
+ /** Reads a coordinate pair [lat,lon]. */
+ public static Coordinate[] readCoordinates(JsonArray arr) {
+ Coordinate[] coords = new Coordinate[arr.size()];
+ for (int i = 0; i < arr.size(); i++)
+ coords[i] = readCoordinate(arr.getJsonArray(i));
+ return coords;
+ }
+
+ /** singleton */
+ private GeoJson() {
+ }
+
+}
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import javax.measure.Quantity;
import javax.measure.quantity.Area;
import org.geotools.styling.css.CssTranslator;
import org.geotools.styling.css.Stylesheet;
import org.geotools.xml.styling.SLDTransformer;
+import org.locationtech.jts.algorithm.hull.ConcaveHull;
import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.geom.util.GeometryFixer;
+import org.locationtech.jts.operation.polygonize.Polygonizer;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Filter;
/** In square meters. */
public static Quantity<Area> calcArea(SimpleFeature feature) {
try {
- Polygon p = (Polygon) feature.getDefaultGeometry();
- Point centroid = p.getCentroid();
+ Polygon polygon = (Polygon) feature.getDefaultGeometry();
+ Point centroid = polygon.getCentroid();
+// CoordinateReferenceSystem crs = feature.getDefaultGeometryProperty().getType()
+// .getCoordinateReferenceSystem();
+ // TODO convert the centroid
+// if (!crs.getName().equals(DefaultGeographicCRS.WGS84.getName()))
+// throw new IllegalArgumentException("Only WGS84 CRS is currently supported");
+
+ // create automatic CRS for optimal computation
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);
+ Polygon projed = (Polygon) JTS.transform(polygon, transform);
return Quantities.getQuantity(projed.getArea(), SI.SQUARE_METRE);
} catch (MismatchedDimensionException | FactoryException | TransformException e) {
- throw new IllegalStateException("Cannot claculate area of feature");
+ throw new IllegalStateException("Cannot calculate area of feature", e);
+ }
+ }
+
+ /** In square meters. The {@link Polygon} must use WGS84 coordinates. */
+ public static Quantity<Area> calcArea(Polygon polygon) {
+ assert polygon.getSRID() == 0 || polygon.getSRID() == 4326;
+ return calcArea(polygon, DefaultGeographicCRS.WGS84, polygon.getCentroid());
+ }
+
+ public static Quantity<Area> calcArea(Polygon polygon, CoordinateReferenceSystem crs, Point wgs84centroid) {
+ try {
+ // create automatic CRS for optimal computation
+ String code = "AUTO:42001," + wgs84centroid.getX() + "," + wgs84centroid.getY();
+ CoordinateReferenceSystem auto = CRS.decode(code);
+
+ MathTransform transform = CRS.findMathTransform(crs, auto);
+
+ Polygon projed = (Polygon) JTS.transform(polygon, transform);
+ return Quantities.getQuantity(projed.getArea(), SI.SQUARE_METRE);
+ } catch (MismatchedDimensionException | FactoryException | TransformException e) {
+ throw new IllegalStateException("Cannot calculate area of feature", e);
}
}
}
public static long getScaleFromResolution(long resolution) {
- // see https://gis.stackexchange.com/questions/242424/how-to-get-map-units-to-find-current-scale-in-openlayers
+ // see
+ // https://gis.stackexchange.com/questions/242424/how-to-get-map-units-to-find-current-scale-in-openlayers
final double INCHES_PER_UNIT = 39.37;// m
// final double INCHES_PER_UNIT = 4374754;// dd
final long DOTS_PER_INCH = 72;
return Math.round(INCHES_PER_UNIT * DOTS_PER_INCH * resolution);
}
+ /*
+ * GEOMETRY
+ */
+ /**
+ * Ensure that a {@link Polygon} is valid and simple by removing artefacts
+ * (typically coming from GPS).
+ *
+ * @return a simple and valid polygon, or null if the ignoredArea ratio is above
+ * the provided threshold.
+ */
+ public static Polygon cleanPolygon(Polygon polygon, double ignoredAreaRatio) {
+ Polygonizer polygonizer = new Polygonizer(true);
+ Geometry fixed = GeometryFixer.fix(polygon, false);
+ polygonizer.add(fixed);
+ @SuppressWarnings("unchecked")
+ List<Polygon> polygons = new ArrayList<>(polygonizer.getPolygons());
+
+ if (polygons.size() == 0) {
+ throw new IllegalStateException("Polygonizer failed to extract any polygon");
+ }
+
+ Polygon best;
+ if (polygons.size() == 1) {
+ best = polygons.get(0);
+ } else {
+ double totalArea = fixed.getArea();
+ best = polygons.get(0);
+ double bestArea = best.getArea();
+ for (int i = 1; i < polygons.size(); i++) {
+ Polygon p = polygons.get(i);
+ double a = p.getArea();
+ if (a > bestArea) {
+ best = p;
+ bestArea = a;
+ } else {
+ // double ratio = a / totalArea;
+ }
+ }
+ double ignoredRatio = (totalArea - bestArea) / totalArea;
+ if (ignoredRatio > ignoredAreaRatio)
+ return null;
+
+ if (!best.isValid() || !best.isSimple()) {
+ throw new IllegalStateException("The polygon is not simple and/or valid after cleaning");
+ }
+ }
+ // while we are here, we make sure that the geometry will be normalised
+ best.normalize();
+ return best;
+ }
+
+ /**
+ * The smallest polygon without holes containing all the points in this
+ * geometry.
+ */
+ public static Polygon concaveHull(Geometry geom, double lengthRatio) {
+ Objects.requireNonNull(geom);
+ if (geom.getNumPoints() < 3)
+ throw new IllegalArgumentException("At least 3 points are reuired to compute the concave hull and geometry "
+ + geom.getClass() + " contains only " + geom.getNumPoints());
+ Geometry hull = ConcaveHull.concaveHullByLengthRatio(geom, lengthRatio, false);
+ if (hull instanceof Polygon polygon)
+ return polygon;
+ else
+ throw new IllegalStateException("Hull is not a polygon but a " + hull.getClass());
+ }
+
/** Singleton. */
private GeoUtils() {
}
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.Polygon;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
*/
@SuppressWarnings("unchecked")
public static <T> T parseGpxTrackTo(InputStream in, Class<T> clss) throws IOException {
- GeometryFactory geometryFactory = JTS.GEOMETRY_FACTORY;
+ GeometryFactory geometryFactory = JTS.GEOMETRY_FACTORY_WGS84;
List<Coordinate> coordinates = new ArrayList<>();
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
LineString lineString = geometryFactory
.createLineString(coordinates.toArray(new Coordinate[coordinates.size()]));
return (T) lineString;
+ } else if (MultiPoint.class.isAssignableFrom(clss)) {
+ MultiPoint multiPoint = geometryFactory
+ .createMultiPointFromCoords(coordinates.toArray(new Coordinate[coordinates.size()]));
+ // multiPoint.normalize();
+ return (T) multiPoint;
} 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()]));
return (T) polygon;
- }
- // TODO MultiPoint? MultiLine? etc.
- else if (SimpleFeature.class.isAssignableFrom(clss)) {
+ } else if (SimpleFeature.class.isAssignableFrom(clss)) {
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(LINESTRING_FEATURE_TYPE);
LineString lineString = geometryFactory
.createLineString(coordinates.toArray(new Coordinate[coordinates.size()]));
throw new IllegalArgumentException("Unsupported format " + clss);
}
}
-
+
/** @deprecated Use {@link #parseGpxTrackTo(InputStream, Class)} instead. */
@Deprecated
public static SimpleFeature parseGpxToPolygon(InputStream in) throws IOException {
import org.argeo.app.api.WGS84PosName;
import org.argeo.app.api.geo.FeatureAdapter;
import org.argeo.app.geo.CqlUtils;
-import org.argeo.app.geo.GeoJSon;
+import org.argeo.app.geo.GeoJson;
import org.argeo.app.geo.GpxUtils;
import org.argeo.app.geo.JTS;
import org.argeo.cms.acr.json.AcrJsonUtils;
String featureId = getFeatureId(c);
if (featureId != null)
generator.write("id", featureId);
- GeoJSon.writeBBox(generator, defaultGeometry);
- generator.writeStartObject(GeoJSon.GEOMETRY);
- GeoJSon.writeGeometry(generator, defaultGeometry);
+// GeoJson.writeBBox(generator, defaultGeometry);
+ generator.writeStartObject(GeoJson.GEOMETRY);
+ GeoJson.writeGeometry(generator, defaultGeometry);
generator.writeEnd();// geometry object
- generator.writeStartObject(GeoJSon.PROPERTIES);
+ generator.writeStartObject(GeoJson.PROPERTIES);
AcrJsonUtils.writeTimeProperties(generator, c);
if (featureAdapter != null)
featureAdapter.writeProperties(generator, c, typeName);