GeometryProcessor.java
/*
* Copyright (C) 2022 B3Partners B.V.
*
* SPDX-License-Identifier: MIT
*/
package org.tailormap.api.geotools.processing;
import jakarta.validation.constraints.NotNull;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import org.geotools.api.referencing.operation.MathTransform;
import org.geotools.api.referencing.operation.TransformException;
import org.geotools.data.geojson.GeoJSONWriter;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.WKTReader2;
import org.geotools.geometry.jts.WKTWriter2;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTConstants;
import org.locationtech.jts.io.WKTWriter;
import org.locationtech.jts.precision.GeometryPrecisionReducer;
import org.locationtech.jts.simplify.TopologyPreservingSimplifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility functions on feature geometries.
*
* @author mprins
* @since 0.1
*/
public final class GeometryProcessor {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private GeometryProcessor() {}
/**
* process the geometry into a (optionally simplified) string representation.
*
* @param geometry An object representing a geometry
* @param simplifyGeometry set to {@code true} to simplify
* @param transform the transformation that should be applied to the geometry, can be {@code null}
* @return the string representation of the argument - normally WKT, optionally simplified or {@code null} when the
* given geometry was {@code null}
*/
@NotNull public static String processGeometry(
Object geometry,
@NotNull final Boolean simplifyGeometry,
@NotNull Boolean linearizeGeomToWKT,
MathTransform transform) {
if (null == geometry) {
return null;
}
if (Geometry.class.isAssignableFrom(geometry.getClass())) {
if (null != transform) {
geometry = transformGeometry((Geometry) geometry, transform);
}
if (simplifyGeometry) {
return simplify((Geometry) geometry);
}
if (linearizeGeomToWKT) {
return linearizeGeomToWKT((Geometry) geometry);
}
}
// cannot cast to JTS geom
return geometry.toString();
}
public static Geometry transformGeometry(@NotNull Geometry geometry, MathTransform transform) {
if (null == transform) {
return geometry;
}
try {
return JTS.transform(geometry, transform);
} catch (TransformException e) {
logger.error("Failed to transform geometry", e);
}
return geometry;
}
private static String linearizeGeomToWKT(Geometry geometry) {
// return linearized WKT
final WKTWriter writer = new WKTWriter(2);
String wkt = writer.write(geometry);
// LINEARRING is non-standard WKT, but the JTS WKTWriter will write it anyway!
if (wkt.startsWith(WKTConstants.LINEARRING)) {
wkt = WKTConstants.LINESTRING + wkt.substring(WKTConstants.LINEARRING.length());
}
return wkt;
}
/**
* Simplifies given geometry to reduce (transfer) size, start off with 1 and each iteration multiply with 10, max 4
* steps, so [1, 10, 100, 1000], if the geometry is still too large bail out and use bbox. TODO this works for CRS
* in meters, may not work for degrees
*
* @param geom geometry to simplify
* @return simplified geometry as WKT string
*/
@NotNull private static String simplify(@NotNull Geometry geom) {
final int megabytes = 2097152 /* 2MB is the default tomcat max post size */ - 100 * 1024;
Geometry bbox = geom.getEnvelope();
logger.trace("PrecisionModel scale: {}", geom.getPrecisionModel().getScale());
GeometryPrecisionReducer gpr = new GeometryPrecisionReducer(new PrecisionModel(geom.getPrecisionModel()));
try {
geom = gpr.reduce(geom);
} catch (IllegalArgumentException e) {
logger.error("Failed to reduce geometry precision", e);
}
double distanceTolerance = 1.0;
String geomTxt = geom.toText();
while ((geomTxt.getBytes(StandardCharsets.UTF_8).length > megabytes || geom.getCoordinates().length > 600)
&& distanceTolerance < 9999) {
logger.debug("Simplify selected feature geometry with distance of: {}", distanceTolerance);
geom = TopologyPreservingSimplifier.simplify(geom, distanceTolerance);
try {
geom = gpr.reduce(geom);
} catch (IllegalArgumentException e) {
logger.error("Failed to reduce geometry precision", e);
}
geomTxt = geom.toText();
distanceTolerance = 10 * distanceTolerance;
}
if (distanceTolerance > 9999) {
logger.debug("Maximum number of simplify cycles reached, returning bounding box instead");
return bbox.toText();
} else {
return linearizeGeomToWKT(geom);
}
}
public static String geometryToJson(Geometry geom) {
return GeoJSONWriter.toGeoJSON(geom);
}
public static String geometryToWKT(@NotNull Geometry geom) {
final int dimension = geom.getDimension() > 1 ? geom.getDimension() : 2;
WKTWriter2 writer = new WKTWriter2(dimension);
return writer.write(geom);
}
public static Geometry wktToGeometry(String wkt) {
if (null != wkt && wkt.length() > 1) {
WKTReader2 reader = new WKTReader2();
try {
return reader.read(wkt);
} catch (ParseException e) {
return null;
}
}
return null;
}
}