From 5dd2dc98b22563b3d7ba6d9ad552cffef43a7d3e Mon Sep 17 00:00:00 2001 From: ncordon Date: Mon, 6 Oct 2025 13:55:13 +0200 Subject: [PATCH 01/11] Adds a first version of st_simplify which is the identity --- x-pack/plugin/esql/build.gradle | 1 + .../function/EsqlFunctionRegistry.java | 2 + .../function/scalar/spatial/StSimplify.java | 109 ++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java diff --git a/x-pack/plugin/esql/build.gradle b/x-pack/plugin/esql/build.gradle index 734c0b62eb729..ab19434a19bc7 100644 --- a/x-pack/plugin/esql/build.gradle +++ b/x-pack/plugin/esql/build.gradle @@ -37,6 +37,7 @@ dependencies { compileOnly project(xpackModule('ml')) compileOnly project(path: xpackModule('mapper-aggregate-metric')) compileOnly project(path: xpackModule('downsample')) + compileOnly "org.locationtech.jts:jts-core:${versions.jts}" implementation project(xpackModule('kql')) implementation project('compute') implementation project('compute:ann') diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index 2d2c644bbcace..163b10ba1b997 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -166,6 +166,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohash; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohex; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeotile; +import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StSimplify; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMax; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMin; @@ -438,6 +439,7 @@ private static FunctionDefinition[][] functions() { def(SpatialWithin.class, SpatialWithin::new, "st_within"), def(StDistance.class, StDistance::new, "st_distance"), def(StEnvelope.class, StEnvelope::new, "st_envelope"), + def(StSimplify.class, StSimplify::new, "st_simplify"), def(StGeohash.class, StGeohash::new, "st_geohash"), def(StGeotile.class, StGeotile::new, "st_geotile"), def(StGeohex.class, StGeohex::new, "st_geohex"), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java new file mode 100644 index 0000000000000..6f6df806d694f --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.spatial; + +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.esql.core.tree.NodeInfo; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper; +import org.elasticsearch.xpack.esql.expression.function.Example; +import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.Param; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE; + +public class StSimplify extends ScalarFunction implements EvaluatorMapper { + Expression geometry; + Expression tolerance; + + @FunctionInfo( + returnType = "geo_shape", + description = "Simplifies the input geometry with a given tolerance", + examples = @Example(file = "spatial", tag = "st_simplify") + ) + public StSimplify(Source source, + @Param( + name = "geometry", + type = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" }, + description = "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. " + + "If `null`, the function returns `null`." + ) Expression geometry, + @Param( + name = "tolerance", + type = { "double" }, + description = "Tolerance for the geometry simplification" + ) + Expression tolerance) { + super(source, List.of(geometry, tolerance)); + this.geometry = geometry; + this.tolerance = tolerance; + } + + @Override + public DataType dataType() { + return GEO_SHAPE; + } + + @Override + public Expression replaceChildren(List newChildren) { + return new StSimplify(source(), newChildren.get(0), newChildren.get(1)); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, StSimplify::new, geometry, tolerance); + } + + @Override + public String getWriteableName() { + return "StSimplify"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + + } + + @Override + public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { + // Get evaluators for the child expressions + EvalOperator.ExpressionEvaluator.Factory geometryEval = toEvaluator.apply(geometry); + EvalOperator.ExpressionEvaluator.Factory toleranceEval = toEvaluator.apply(tolerance); + + return dvrCtx -> { + EvalOperator.ExpressionEvaluator geometryEvaluator = geometryEval.get(dvrCtx); + // toleranceEvaluator is not used since we just return the geometry + + return new EvalOperator.ExpressionEvaluator() { + @Override + public void close() { + + } + + @Override + public Block eval(Page page) { + return geometryEvaluator.eval(page); + } + + @Override + public long baseRamBytesUsed() { + return 0; + } + }; + }; + } +} From b787a477cb68414b571e030e5b2508982e644e00 Mon Sep 17 00:00:00 2001 From: ncordon Date: Tue, 7 Oct 2025 11:39:45 +0200 Subject: [PATCH 02/11] This does not work :____ --- x-pack/plugin/esql/build.gradle | 2 +- .../function/scalar/spatial/StSimplify.java | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/esql/build.gradle b/x-pack/plugin/esql/build.gradle index ab19434a19bc7..9aeaa789bf9e0 100644 --- a/x-pack/plugin/esql/build.gradle +++ b/x-pack/plugin/esql/build.gradle @@ -37,7 +37,7 @@ dependencies { compileOnly project(xpackModule('ml')) compileOnly project(path: xpackModule('mapper-aggregate-metric')) compileOnly project(path: xpackModule('downsample')) - compileOnly "org.locationtech.jts:jts-core:${versions.jts}" + implementation "org.locationtech.jts:jts-core:${versions.jts}" implementation project(xpackModule('kql')) implementation project('compute') implementation project('compute:ann') diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java index 6f6df806d694f..7d729d18ef23b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -7,10 +7,13 @@ package org.elasticsearch.xpack.esql.expression.function.scalar.spatial; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.DoubleBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -20,11 +23,17 @@ import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.expression.function.scalar.math.AbsDoubleEvaluator; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.io.WKTWriter; +import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; import java.io.IOException; import java.util.List; import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE; +import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.makeGeometryFromLiteral; public class StSimplify extends ScalarFunction implements EvaluatorMapper { Expression geometry; @@ -86,7 +95,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua return dvrCtx -> { EvalOperator.ExpressionEvaluator geometryEvaluator = geometryEval.get(dvrCtx); - // toleranceEvaluator is not used since we just return the geometry + EvalOperator.ExpressionEvaluator toleranceEvaluator = toleranceEval.get(dvrCtx); return new EvalOperator.ExpressionEvaluator() { @Override @@ -96,6 +105,30 @@ public void close() { @Override public Block eval(Page page) { + var isGeometryFoldable = geometry.foldable(); + var isToleranceFoldable = tolerance.foldable(); + + if (isGeometryFoldable) { + var esGeometry = makeGeometryFromLiteral(toEvaluator.foldCtx(), geometry); + DoubleBlock fieldValBlock = (DoubleBlock) toleranceEvaluator.eval(page); + String wkt = WellKnownText.toWKT(esGeometry); + WKTReader reader = new WKTReader(); + try { + org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); + + org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, 0); + WKTWriter writer = new WKTWriter(); + String simplifiedWkt = writer.write(simplifiedGeometry); + var numBytes = simplifiedWkt.getBytes().length; + var builder = dvrCtx.blockFactory().newBytesRefBlockBuilder(numBytes); + builder.appendBytesRef(new BytesRef(simplifiedWkt)); + return builder.build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + + } return geometryEvaluator.eval(page); } From 4413503bb66548ac15fb8fae68afae1e737b7b6c Mon Sep 17 00:00:00 2001 From: ncordon Date: Tue, 7 Oct 2025 14:59:03 +0200 Subject: [PATCH 03/11] Adds first working version, but only for foldable geometries --- .../function/scalar/spatial/StSimplify.java | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java index 7d729d18ef23b..3245f0a9e6c32 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -10,9 +10,11 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.data.Block; -import org.elasticsearch.compute.data.DoubleBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.utils.StandardValidator; +import org.elasticsearch.geometry.utils.WellKnownBinary; import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction; @@ -23,13 +25,12 @@ import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.Param; -import org.elasticsearch.xpack.esql.expression.function.scalar.math.AbsDoubleEvaluator; -import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.WKTReader; import org.locationtech.jts.io.WKTWriter; import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; import java.io.IOException; +import java.nio.ByteOrder; import java.util.List; import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE; @@ -44,19 +45,16 @@ public class StSimplify extends ScalarFunction implements EvaluatorMapper { description = "Simplifies the input geometry with a given tolerance", examples = @Example(file = "spatial", tag = "st_simplify") ) - public StSimplify(Source source, + public StSimplify( + Source source, @Param( name = "geometry", type = { "geo_point", "geo_shape", "cartesian_point", "cartesian_shape" }, description = "Expression of type `geo_point`, `geo_shape`, `cartesian_point` or `cartesian_shape`. " + "If `null`, the function returns `null`." ) Expression geometry, - @Param( - name = "tolerance", - type = { "double" }, - description = "Tolerance for the geometry simplification" - ) - Expression tolerance) { + @Param(name = "tolerance", type = { "double" }, description = "Tolerance for the geometry simplification") Expression tolerance + ) { super(source, List.of(geometry, tolerance)); this.geometry = geometry; this.tolerance = tolerance; @@ -107,28 +105,37 @@ public void close() { public Block eval(Page page) { var isGeometryFoldable = geometry.foldable(); var isToleranceFoldable = tolerance.foldable(); + var positionCount = page.getPositionCount(); if (isGeometryFoldable) { var esGeometry = makeGeometryFromLiteral(toEvaluator.foldCtx(), geometry); - DoubleBlock fieldValBlock = (DoubleBlock) toleranceEvaluator.eval(page); String wkt = WellKnownText.toWKT(esGeometry); WKTReader reader = new WKTReader(); - try { - org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); - - org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, 0); - WKTWriter writer = new WKTWriter(); - String simplifiedWkt = writer.write(simplifiedGeometry); - var numBytes = simplifiedWkt.getBytes().length; - var builder = dvrCtx.blockFactory().newBytesRefBlockBuilder(numBytes); - builder.appendBytesRef(new BytesRef(simplifiedWkt)); - return builder.build(); - } catch (Exception e) { - throw new RuntimeException(e); - } - + try (var result = dvrCtx.blockFactory().newBytesRefVectorBuilder(positionCount)) { + for (int p = 0; p < positionCount; p++) { + try { + org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); + org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify( + jtsGeometry, + 0 + ); + WKTWriter writer = new WKTWriter(); + String simplifiedWkt = writer.write(simplifiedGeometry); + Geometry esGeometryResult = WellKnownText.fromWKT( + StandardValidator.instance(true), + false, + simplifiedWkt + ); + result.appendBytesRef(new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN))); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return result.build().asBlock(); + } } + // identity return geometryEvaluator.eval(page); } From 805ab8939454d28de7d3d01e7d6ff57e3bb7f09a Mon Sep 17 00:00:00 2001 From: ncordon Date: Tue, 7 Oct 2025 15:59:32 +0200 Subject: [PATCH 04/11] Adds implementation for non foldable geometry --- .../function/scalar/spatial/StSimplify.java | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java index 3245f0a9e6c32..5be397845b620 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -10,6 +10,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefVectorBlock; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.geometry.Geometry; @@ -87,13 +88,9 @@ public void writeTo(StreamOutput out) throws IOException { @Override public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { - // Get evaluators for the child expressions - EvalOperator.ExpressionEvaluator.Factory geometryEval = toEvaluator.apply(geometry); - EvalOperator.ExpressionEvaluator.Factory toleranceEval = toEvaluator.apply(tolerance); - return dvrCtx -> { - EvalOperator.ExpressionEvaluator geometryEvaluator = geometryEval.get(dvrCtx); - EvalOperator.ExpressionEvaluator toleranceEvaluator = toleranceEval.get(dvrCtx); + EvalOperator.ExpressionEvaluator geometryEvaluator = toEvaluator.apply(geometry).get(dvrCtx); + EvalOperator.ExpressionEvaluator toleranceEvaluator = toEvaluator.apply(tolerance).get(dvrCtx); return new EvalOperator.ExpressionEvaluator() { @Override @@ -106,37 +103,52 @@ public Block eval(Page page) { var isGeometryFoldable = geometry.foldable(); var isToleranceFoldable = tolerance.foldable(); var positionCount = page.getPositionCount(); - - if (isGeometryFoldable) { - var esGeometry = makeGeometryFromLiteral(toEvaluator.foldCtx(), geometry); - String wkt = WellKnownText.toWKT(esGeometry); - WKTReader reader = new WKTReader(); - - try (var result = dvrCtx.blockFactory().newBytesRefVectorBuilder(positionCount)) { - for (int p = 0; p < positionCount; p++) { - try { + var validator = StandardValidator.instance(true); + WKTReader reader = new WKTReader(); + WKTWriter writer = new WKTWriter(); + + // TODO We are not extracting non foldable geometries + // TODO We are not using the tolerance + try (var result = dvrCtx.blockFactory().newBytesRefVectorBuilder(positionCount)) { + if (isGeometryFoldable) { + var esGeometry = makeGeometryFromLiteral(toEvaluator.foldCtx(), geometry); + String wkt = WellKnownText.toWKT(esGeometry); + + try { + org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); + org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, 0); + String simplifiedWkt = writer.write(simplifiedGeometry); + Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); + + for (int p = 0; p < positionCount; p++) { + result.appendBytesRef(new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN))); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + // Geometry is non foldable + BytesRefVectorBlock block = (BytesRefVectorBlock) geometryEvaluator.eval(page); + var bytesRefVector = block.asVector(); + + try { + for (int p = 0; p < positionCount; p++) { + var destRef = bytesRefVector.getBytesRef(p, new BytesRef()); + var wkt = WellKnownText.fromWKB(destRef.bytes, destRef.offset, destRef.length); org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); - org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify( - jtsGeometry, - 0 - ); - WKTWriter writer = new WKTWriter(); + org.locationtech.jts.geom.Geometry simplifiedGeometry = + DouglasPeuckerSimplifier.simplify(jtsGeometry, 0); String simplifiedWkt = writer.write(simplifiedGeometry); - Geometry esGeometryResult = WellKnownText.fromWKT( - StandardValidator.instance(true), - false, - simplifiedWkt - ); + Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); result.appendBytesRef(new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN))); - } catch (Exception e) { - throw new RuntimeException(e); } + } catch (Exception e) { + throw new RuntimeException(e); } - return result.build().asBlock(); } + + return result.build().asBlock(); } - // identity - return geometryEvaluator.eval(page); } @Override From ed96b60f0273f8ad830f184e9077b8cd8252fa14 Mon Sep 17 00:00:00 2001 From: ncordon Date: Wed, 8 Oct 2025 18:20:10 +0200 Subject: [PATCH 05/11] Adds tests --- .../src/main/resources/spatial.csv-spec | 72 +++++++++++++++++++ .../xpack/esql/action/EsqlCapabilities.java | 5 ++ .../esql/expression/ExpressionWritables.java | 4 +- .../function/scalar/spatial/StSimplify.java | 47 +++++++++--- 4 files changed, 116 insertions(+), 12 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec index 34e5c2394ff54..e5c7ecd8320a6 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec @@ -3024,3 +3024,75 @@ wkt:keyword |pt:cartesian_point "POINT(111)" |null // end::to_cartesianpoint-str-parse-error-result[] ; + +stSimplifyMultiRow +required_capability: st_simplify + +FROM airports +| SORT name +| LIMIT 5 +| EVAL result = st_simplify(TO_GEOSHAPE("POLYGON((-10 -60, 120 -60, 120 60, -10 60, -10 -60))"), 1.0) +| KEEP name, result +; + +name:text | result:geo_shape +Aba Tenna D. Yilma Int'l | POLYGON ((-10.0 -60.0, -10.0 60.0, 120.0 60.0, 120.0 -60.0, -10.0 -60.0)) +Abdul Rachman Saleh | POLYGON ((-10.0 -60.0, -10.0 60.0, 120.0 60.0, 120.0 -60.0, -10.0 -60.0)) +Abidjan Port Bouet | POLYGON ((-10.0 -60.0, -10.0 60.0, 120.0 60.0, 120.0 -60.0, -10.0 -60.0)) +Abu Dhabi Int'l | POLYGON ((-10.0 -60.0, -10.0 60.0, 120.0 60.0, 120.0 -60.0, -10.0 -60.0)) +Abuja Int'l | POLYGON ((-10.0 -60.0, -10.0 60.0, 120.0 60.0, 120.0 -60.0, -10.0 -60.0)) +; + +stSimplifyMultiRowWithPoints +required_capability: st_simplify + +FROM airports +| SORT name +| LIMIT 5 +| EVAL result = st_simplify(location, 0.0) +| KEEP location, result +; + +location:geo_point | result:geo_shape +POINT (41.857756722253 9.61267784753569) | POINT (41.857756722253 9.61267784753569) +POINT (112.711418617258 -7.92998002840567) | POINT (112.711418617258 -7.92998002840567) +POINT (-3.93221929167636 5.2543984451492) | POINT (-3.93221929167636 5.2543984451492) +POINT (54.6463293225558 24.4272271529764) | POINT (54.6463293225558 24.4272271529764) +POINT (7.27025993974356 9.00437659781094) | POINT (7.27025993974356 9.00437659781094) +; + +stSimplifyNoSimplification +required_capability: st_simplify + +ROW geo_shape = TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))") +| EVAL result = st_simplify(geo_shape, 0.01) +| KEEP result +; + +result:geo_shape +POLYGON ((0.0 0.0, 0.0 2.0, 1.0 1.9, 2.0 2.0, 2.0 0.0, 1.0 0.1, 0.0 0.0)) +; + +stSimplifyWithSimplification +required_capability: st_simplify + +ROW geo_shape = TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))") +| EVAL result = st_simplify(geo_shape, 0.2) +| KEEP result +; + +result:geo_shape +POLYGON ((0.0 0.0, 0.0 2.0, 2.0 2.0, 2.0 0.0, 0.0 0.0)) +; + +stSimplifyEmptySimplification +required_capability: st_simplify + +ROW geo_shape = TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))") +| EVAL result = st_simplify(geo_shape, 2.0) +| KEEP result +; + +result:geo_shape +POLYGON EMPTY +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 81bc6a5d89a0f..c568d71c20312 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -78,6 +78,11 @@ public enum Cap { */ ST_DISJOINT, + /** + * Support for spatial simplification {@code ST_SIMPLIFY} + */ + ST_SIMPLIFY, + /** * The introduction of the {@code VALUES} agg. */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java index ed7e8ebb57003..38d538af17c3e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java @@ -71,6 +71,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohash; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohex; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeotile; +import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StSimplify; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StX; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMax; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StXMin; @@ -253,7 +254,8 @@ private static List spatials() { StDistance.ENTRY, StGeohash.ENTRY, StGeotile.ENTRY, - StGeohex.ENTRY + StGeohex.ENTRY, + StSimplify.ENTRY ); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java index 5be397845b620..21ec65e897c51 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -8,9 +8,12 @@ package org.elasticsearch.xpack.esql.expression.function.scalar.spatial; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.BytesRefVectorBlock; +import org.elasticsearch.compute.data.DoubleVector; import org.elasticsearch.compute.data.Page; import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.geometry.Geometry; @@ -18,14 +21,14 @@ import org.elasticsearch.geometry.utils.WellKnownBinary; import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; -import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; +import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.locationtech.jts.io.WKTReader; import org.locationtech.jts.io.WKTWriter; import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; @@ -37,7 +40,12 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE; import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.makeGeometryFromLiteral; -public class StSimplify extends ScalarFunction implements EvaluatorMapper { +public class StSimplify extends EsqlScalarFunction { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( + Expression.class, + "StSimplify", + StSimplify::new + ); Expression geometry; Expression tolerance; @@ -61,6 +69,14 @@ public StSimplify( this.tolerance = tolerance; } + private StSimplify(StreamInput in) throws IOException { + this( + Source.readFrom((PlanStreamInput) in), + in.readNamedWriteable(Expression.class), + in.readNamedWriteable(Expression.class) + ); + } + @Override public DataType dataType() { return GEO_SHAPE; @@ -78,12 +94,14 @@ protected NodeInfo info() { @Override public String getWriteableName() { - return "StSimplify"; + return ENTRY.name; } @Override public void writeTo(StreamOutput out) throws IOException { - + source().writeTo(out); + out.writeNamedWriteable(geometry); + out.writeNamedWriteable(tolerance); } @Override @@ -101,11 +119,11 @@ public void close() { @Override public Block eval(Page page) { var isGeometryFoldable = geometry.foldable(); - var isToleranceFoldable = tolerance.foldable(); var positionCount = page.getPositionCount(); var validator = StandardValidator.instance(true); WKTReader reader = new WKTReader(); WKTWriter writer = new WKTWriter(); + DoubleVector tolerances = (DoubleVector) toleranceEvaluator.eval(page).asVector(); // TODO We are not extracting non foldable geometries // TODO We are not using the tolerance @@ -116,11 +134,13 @@ public Block eval(Page page) { try { org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); - org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, 0); - String simplifiedWkt = writer.write(simplifiedGeometry); - Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); for (int p = 0; p < positionCount; p++) { + double distanceTolerance = tolerances.getDouble(p); + org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, distanceTolerance); + String simplifiedWkt = writer.write(simplifiedGeometry); + Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); + result.appendBytesRef(new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN))); } } catch (Exception e) { @@ -133,11 +153,14 @@ public Block eval(Page page) { try { for (int p = 0; p < positionCount; p++) { + double distanceTolerance = tolerances.getDouble(p); var destRef = bytesRefVector.getBytesRef(p, new BytesRef()); var wkt = WellKnownText.fromWKB(destRef.bytes, destRef.offset, destRef.length); org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); - org.locationtech.jts.geom.Geometry simplifiedGeometry = - DouglasPeuckerSimplifier.simplify(jtsGeometry, 0); + org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify( + jtsGeometry, + distanceTolerance + ); String simplifiedWkt = writer.write(simplifiedGeometry); Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); result.appendBytesRef(new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN))); @@ -145,7 +168,9 @@ public Block eval(Page page) { } catch (Exception e) { throw new RuntimeException(e); } + block.close(); } + tolerances.close(); return result.build().asBlock(); } From e5e3d445557f416846e9790c2d17358a703b9e67 Mon Sep 17 00:00:00 2001 From: ncordon Date: Thu, 9 Oct 2025 15:12:09 +0200 Subject: [PATCH 06/11] Checkpoint --- .../function/scalar/spatial/StSimplify.java | 139 +++++++----------- 1 file changed, 53 insertions(+), 86 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java index 21ec65e897c51..8cd1d4e60ea46 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -11,12 +11,13 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.compute.data.Block; -import org.elasticsearch.compute.data.BytesRefVectorBlock; -import org.elasticsearch.compute.data.DoubleVector; -import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.ann.ConvertEvaluator; +import org.elasticsearch.compute.ann.Evaluator; +import org.elasticsearch.compute.ann.Fixed; +import org.elasticsearch.compute.data.BytesRefBlock; import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.utils.GeometryValidator; import org.elasticsearch.geometry.utils.StandardValidator; import org.elasticsearch.geometry.utils.WellKnownBinary; import org.elasticsearch.geometry.utils.WellKnownText; @@ -38,7 +39,6 @@ import java.util.List; import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE; -import static org.elasticsearch.xpack.esql.expression.function.scalar.spatial.SpatialRelatesUtils.makeGeometryFromLiteral; public class StSimplify extends EsqlScalarFunction { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( @@ -70,11 +70,7 @@ public StSimplify( } private StSimplify(StreamInput in) throws IOException { - this( - Source.readFrom((PlanStreamInput) in), - in.readNamedWriteable(Expression.class), - in.readNamedWriteable(Expression.class) - ); + this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), in.readNamedWriteable(Expression.class)); } @Override @@ -104,83 +100,54 @@ public void writeTo(StreamOutput out) throws IOException { out.writeNamedWriteable(tolerance); } + private static class GeoSimplifier { + static GeometryValidator validator = StandardValidator.instance(true); + static WKTReader reader = new WKTReader(); + static WKTWriter writer = new WKTWriter(); + + public static BytesRef geoSourceAndConstantTolerance(BytesRef inputGeometry, @Fixed double inputTolerance) { + String wkt = WellKnownText.fromWKB(inputGeometry.bytes, inputGeometry.offset, inputGeometry.length); + try { + org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); + org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, inputTolerance); + String simplifiedWkt = writer.write(simplifiedGeometry); + Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); + return new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + @Override public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { - return dvrCtx -> { - EvalOperator.ExpressionEvaluator geometryEvaluator = toEvaluator.apply(geometry).get(dvrCtx); - EvalOperator.ExpressionEvaluator toleranceEvaluator = toEvaluator.apply(tolerance).get(dvrCtx); - - return new EvalOperator.ExpressionEvaluator() { - @Override - public void close() { - - } - - @Override - public Block eval(Page page) { - var isGeometryFoldable = geometry.foldable(); - var positionCount = page.getPositionCount(); - var validator = StandardValidator.instance(true); - WKTReader reader = new WKTReader(); - WKTWriter writer = new WKTWriter(); - DoubleVector tolerances = (DoubleVector) toleranceEvaluator.eval(page).asVector(); - - // TODO We are not extracting non foldable geometries - // TODO We are not using the tolerance - try (var result = dvrCtx.blockFactory().newBytesRefVectorBuilder(positionCount)) { - if (isGeometryFoldable) { - var esGeometry = makeGeometryFromLiteral(toEvaluator.foldCtx(), geometry); - String wkt = WellKnownText.toWKT(esGeometry); - - try { - org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); - - for (int p = 0; p < positionCount; p++) { - double distanceTolerance = tolerances.getDouble(p); - org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, distanceTolerance); - String simplifiedWkt = writer.write(simplifiedGeometry); - Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); - - result.appendBytesRef(new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN))); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } else { - // Geometry is non foldable - BytesRefVectorBlock block = (BytesRefVectorBlock) geometryEvaluator.eval(page); - var bytesRefVector = block.asVector(); - - try { - for (int p = 0; p < positionCount; p++) { - double distanceTolerance = tolerances.getDouble(p); - var destRef = bytesRefVector.getBytesRef(p, new BytesRef()); - var wkt = WellKnownText.fromWKB(destRef.bytes, destRef.offset, destRef.length); - org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); - org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify( - jtsGeometry, - distanceTolerance - ); - String simplifiedWkt = writer.write(simplifiedGeometry); - Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); - result.appendBytesRef(new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN))); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - block.close(); - } - tolerances.close(); - - return result.build().asBlock(); - } - } - - @Override - public long baseRamBytesUsed() { - return 0; - } - }; - }; + EvalOperator.ExpressionEvaluator.Factory geometryEvaluator = toEvaluator.apply(geometry); + + if (tolerance.foldable() == false) { + throw new IllegalArgumentException("tolerance must be foldable"); + } + double inputTolerance = (double) tolerance.fold(toEvaluator.foldCtx()); + + if (geometry.foldable()) { + BytesRef inputGeometry = (BytesRef) geometry.fold(toEvaluator.foldCtx()); + return new StSimplifyFoldableGeoAndConstantToleranceEvaluator.Factory(source(), inputGeometry, inputTolerance); + } + return new StSimplifyNonFoldableGeoAndConstantToleranceEvaluator.Factory(source(), geometryEvaluator, inputTolerance); + } + + @Evaluator(extraName = "NonFoldableGeoAndConstantTolerance", warnExceptions = { IllegalArgumentException.class }) + static BytesRef processNonFoldableGeoAndConstantTolerance( + BytesRef inputGeometry, + @Fixed double inputTolerance + ) { + return GeoSimplifier.geoSourceAndConstantTolerance(inputGeometry, inputTolerance); + } + + @Evaluator(extraName = "FoldableGeoAndConstantTolerance", warnExceptions = { IllegalArgumentException.class }) + static BytesRef processFoldableGeoAndConstantTolerance( + @Fixed BytesRef inputGeometry, + @Fixed double inputTolerance + ) { + return GeoSimplifier.geoSourceAndConstantTolerance(inputGeometry, inputTolerance); } } From ac2e0be5dcdff7962113a8ea6f9fe7f1ba0f3300 Mon Sep 17 00:00:00 2001 From: ncordon Date: Thu, 9 Oct 2025 18:20:51 +0200 Subject: [PATCH 07/11] Adds more tests --- .../plugin/esql/licenses/jts-core-LICENSE.txt | 31 ++++ .../plugin/esql/licenses/jts-core-NOTICE.txt | 1 + .../src/main/resources/spatial.csv-spec | 48 ++++++ ...dableGeoAndConstantToleranceEvaluator.java | 114 +++++++++++++ ...dableGeoAndConstantToleranceEvaluator.java | 155 ++++++++++++++++++ .../function/scalar/spatial/StSimplify.java | 25 +-- 6 files changed, 354 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugin/esql/licenses/jts-core-LICENSE.txt create mode 100644 x-pack/plugin/esql/licenses/jts-core-NOTICE.txt create mode 100644 x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyFoldableGeoAndConstantToleranceEvaluator.java create mode 100644 x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeoAndConstantToleranceEvaluator.java diff --git a/x-pack/plugin/esql/licenses/jts-core-LICENSE.txt b/x-pack/plugin/esql/licenses/jts-core-LICENSE.txt new file mode 100644 index 0000000000000..bc03db03a5926 --- /dev/null +++ b/x-pack/plugin/esql/licenses/jts-core-LICENSE.txt @@ -0,0 +1,31 @@ +Eclipse Distribution License - v 1.0 + +Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + Neither the name of the Eclipse Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/x-pack/plugin/esql/licenses/jts-core-NOTICE.txt b/x-pack/plugin/esql/licenses/jts-core-NOTICE.txt new file mode 100644 index 0000000000000..8d1c8b69c3fce --- /dev/null +++ b/x-pack/plugin/esql/licenses/jts-core-NOTICE.txt @@ -0,0 +1 @@ + diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec index e5c7ecd8320a6..fd8e19370e957 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec @@ -3025,6 +3025,10 @@ wkt:keyword |pt:cartesian_point // end::to_cartesianpoint-str-parse-error-result[] ; +############################################### +# Tests for ST_SIMPLIFY +############################################### + stSimplifyMultiRow required_capability: st_simplify @@ -3096,3 +3100,47 @@ ROW geo_shape = TO_GEOSHAPE("POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))") result:geo_shape POLYGON EMPTY ; + +stSimplifyNull +required_capability: st_simplify + +ROW geo_shape = NULL +| EVAL result = st_simplify(geo_shape, 2.0) +| KEEP result +; + +result:geo_shape +NULL +; + +stSimplifyCartesianPoint +required_capability: st_simplify + +# TODO Why cannot we use a latitud outside of -90, 90 when there are tests +# that use the TO_CARTESIANPOINT that are doing it already? +ROW wkt = ["POINT(97.11 75.53)", "POINT(80.93 72.77)"] +| MV_EXPAND wkt +| EVAL pt = TO_CARTESIANPOINT(wkt) +| EVAL result = st_simplify(pt, 2.0) +| KEEP result +; + +result:geo_shape +POINT (97.11 75.53) +POINT (80.93 72.77) +; + +stSimplifyCartesianShape +required_capability: st_simplify + +ROW wkt = ["POINT(97.11 75.53)", "POLYGON((0 0, 1 0.1, 2 0, 2 2, 1 1.9, 0 2, 0 0))"] +| MV_EXPAND wkt +| EVAL geom = TO_CARTESIANSHAPE(wkt) +| EVAL result = st_simplify(geom, 0.2) +| KEEP result +; + +result:geo_shape +POINT (97.11 75.53) +POLYGON ((0.0 0.0, 0.0 2.0, 2.0 2.0, 2.0 0.0, 0.0 0.0)) +; diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyFoldableGeoAndConstantToleranceEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyFoldableGeoAndConstantToleranceEvaluator.java new file mode 100644 index 0000000000000..5bcde39b51a91 --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyFoldableGeoAndConstantToleranceEvaluator.java @@ -0,0 +1,114 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.spatial; + +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.RamUsageEstimator; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StSimplify}. + * This class is generated. Edit {@code EvaluatorImplementer} instead. + */ +public final class StSimplifyFoldableGeoAndConstantToleranceEvaluator implements EvalOperator.ExpressionEvaluator { + private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(StSimplifyFoldableGeoAndConstantToleranceEvaluator.class); + + private final Source source; + + private final BytesRef inputGeometry; + + private final double inputTolerance; + + private final DriverContext driverContext; + + private Warnings warnings; + + public StSimplifyFoldableGeoAndConstantToleranceEvaluator(Source source, BytesRef inputGeometry, + double inputTolerance, DriverContext driverContext) { + this.source = source; + this.inputGeometry = inputGeometry; + this.inputTolerance = inputTolerance; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + return eval(page.getPositionCount()); + } + + @Override + public long baseRamBytesUsed() { + long baseRamBytesUsed = BASE_RAM_BYTES_USED; + return baseRamBytesUsed; + } + + public BytesRefBlock eval(int positionCount) { + try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + try { + result.appendBytesRef(StSimplify.processFoldableGeoAndConstantTolerance(this.inputGeometry, this.inputTolerance)); + } catch (IllegalArgumentException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + @Override + public String toString() { + return "StSimplifyFoldableGeoAndConstantToleranceEvaluator[" + "inputGeometry=" + inputGeometry + ", inputTolerance=" + inputTolerance + "]"; + } + + @Override + public void close() { + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final BytesRef inputGeometry; + + private final double inputTolerance; + + public Factory(Source source, BytesRef inputGeometry, double inputTolerance) { + this.source = source; + this.inputGeometry = inputGeometry; + this.inputTolerance = inputTolerance; + } + + @Override + public StSimplifyFoldableGeoAndConstantToleranceEvaluator get(DriverContext context) { + return new StSimplifyFoldableGeoAndConstantToleranceEvaluator(source, inputGeometry, inputTolerance, context); + } + + @Override + public String toString() { + return "StSimplifyFoldableGeoAndConstantToleranceEvaluator[" + "inputGeometry=" + inputGeometry + ", inputTolerance=" + inputTolerance + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeoAndConstantToleranceEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeoAndConstantToleranceEvaluator.java new file mode 100644 index 0000000000000..909fc4eb35d83 --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplifyNonFoldableGeoAndConstantToleranceEvaluator.java @@ -0,0 +1,155 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.spatial; + +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.RamUsageEstimator; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link StSimplify}. + * This class is generated. Edit {@code EvaluatorImplementer} instead. + */ +public final class StSimplifyNonFoldableGeoAndConstantToleranceEvaluator implements EvalOperator.ExpressionEvaluator { + private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(StSimplifyNonFoldableGeoAndConstantToleranceEvaluator.class); + + private final Source source; + + private final EvalOperator.ExpressionEvaluator inputGeometry; + + private final double inputTolerance; + + private final DriverContext driverContext; + + private Warnings warnings; + + public StSimplifyNonFoldableGeoAndConstantToleranceEvaluator(Source source, + EvalOperator.ExpressionEvaluator inputGeometry, double inputTolerance, + DriverContext driverContext) { + this.source = source; + this.inputGeometry = inputGeometry; + this.inputTolerance = inputTolerance; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + try (BytesRefBlock inputGeometryBlock = (BytesRefBlock) inputGeometry.eval(page)) { + BytesRefVector inputGeometryVector = inputGeometryBlock.asVector(); + if (inputGeometryVector == null) { + return eval(page.getPositionCount(), inputGeometryBlock); + } + return eval(page.getPositionCount(), inputGeometryVector); + } + } + + @Override + public long baseRamBytesUsed() { + long baseRamBytesUsed = BASE_RAM_BYTES_USED; + baseRamBytesUsed += inputGeometry.baseRamBytesUsed(); + return baseRamBytesUsed; + } + + public BytesRefBlock eval(int positionCount, BytesRefBlock inputGeometryBlock) { + try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { + BytesRef inputGeometryScratch = new BytesRef(); + position: for (int p = 0; p < positionCount; p++) { + if (inputGeometryBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (inputGeometryBlock.getValueCount(p) != 1) { + if (inputGeometryBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + BytesRef inputGeometry = inputGeometryBlock.getBytesRef(inputGeometryBlock.getFirstValueIndex(p), inputGeometryScratch); + try { + result.appendBytesRef(StSimplify.processNonFoldableGeoAndConstantTolerance(inputGeometry, this.inputTolerance)); + } catch (IllegalArgumentException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + public BytesRefBlock eval(int positionCount, BytesRefVector inputGeometryVector) { + try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { + BytesRef inputGeometryScratch = new BytesRef(); + position: for (int p = 0; p < positionCount; p++) { + BytesRef inputGeometry = inputGeometryVector.getBytesRef(p, inputGeometryScratch); + try { + result.appendBytesRef(StSimplify.processNonFoldableGeoAndConstantTolerance(inputGeometry, this.inputTolerance)); + } catch (IllegalArgumentException e) { + warnings().registerException(e); + result.appendNull(); + } + } + return result.build(); + } + } + + @Override + public String toString() { + return "StSimplifyNonFoldableGeoAndConstantToleranceEvaluator[" + "inputGeometry=" + inputGeometry + ", inputTolerance=" + inputTolerance + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(inputGeometry); + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final EvalOperator.ExpressionEvaluator.Factory inputGeometry; + + private final double inputTolerance; + + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory inputGeometry, + double inputTolerance) { + this.source = source; + this.inputGeometry = inputGeometry; + this.inputTolerance = inputTolerance; + } + + @Override + public StSimplifyNonFoldableGeoAndConstantToleranceEvaluator get(DriverContext context) { + return new StSimplifyNonFoldableGeoAndConstantToleranceEvaluator(source, inputGeometry.get(context), inputTolerance, context); + } + + @Override + public String toString() { + return "StSimplifyNonFoldableGeoAndConstantToleranceEvaluator[" + "inputGeometry=" + inputGeometry + ", inputTolerance=" + inputTolerance + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java index 8cd1d4e60ea46..6fd6f0f253a4d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -11,16 +11,9 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.compute.ann.ConvertEvaluator; import org.elasticsearch.compute.ann.Evaluator; import org.elasticsearch.compute.ann.Fixed; -import org.elasticsearch.compute.data.BytesRefBlock; import org.elasticsearch.compute.operator.EvalOperator; -import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.utils.GeometryValidator; -import org.elasticsearch.geometry.utils.StandardValidator; -import org.elasticsearch.geometry.utils.WellKnownBinary; -import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -35,10 +28,10 @@ import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; import java.io.IOException; -import java.nio.ByteOrder; import java.util.List; import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_SHAPE; +import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.UNSPECIFIED; public class StSimplify extends EsqlScalarFunction { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( @@ -101,18 +94,16 @@ public void writeTo(StreamOutput out) throws IOException { } private static class GeoSimplifier { - static GeometryValidator validator = StandardValidator.instance(true); static WKTReader reader = new WKTReader(); static WKTWriter writer = new WKTWriter(); public static BytesRef geoSourceAndConstantTolerance(BytesRef inputGeometry, @Fixed double inputTolerance) { - String wkt = WellKnownText.fromWKB(inputGeometry.bytes, inputGeometry.offset, inputGeometry.length); + String wkt = UNSPECIFIED.wkbToWkt(inputGeometry); try { org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, inputTolerance); String simplifiedWkt = writer.write(simplifiedGeometry); - Geometry esGeometryResult = WellKnownText.fromWKT(validator, false, simplifiedWkt); - return new BytesRef(WellKnownBinary.toWKB(esGeometryResult, ByteOrder.LITTLE_ENDIAN)); + return UNSPECIFIED.wktToWkb(simplifiedWkt); } catch (Exception e) { throw new RuntimeException(e); } @@ -136,18 +127,12 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua } @Evaluator(extraName = "NonFoldableGeoAndConstantTolerance", warnExceptions = { IllegalArgumentException.class }) - static BytesRef processNonFoldableGeoAndConstantTolerance( - BytesRef inputGeometry, - @Fixed double inputTolerance - ) { + static BytesRef processNonFoldableGeoAndConstantTolerance(BytesRef inputGeometry, @Fixed double inputTolerance) { return GeoSimplifier.geoSourceAndConstantTolerance(inputGeometry, inputTolerance); } @Evaluator(extraName = "FoldableGeoAndConstantTolerance", warnExceptions = { IllegalArgumentException.class }) - static BytesRef processFoldableGeoAndConstantTolerance( - @Fixed BytesRef inputGeometry, - @Fixed double inputTolerance - ) { + static BytesRef processFoldableGeoAndConstantTolerance(@Fixed BytesRef inputGeometry, @Fixed double inputTolerance) { return GeoSimplifier.geoSourceAndConstantTolerance(inputGeometry, inputTolerance); } } From 0e6e1e23a0c75c49df936b3559f6cda1e0aeff31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Cord=C3=B3n?= Date: Thu, 9 Oct 2025 18:23:14 +0200 Subject: [PATCH 08/11] Update docs/changelog/136309.yaml --- docs/changelog/136309.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/136309.yaml diff --git a/docs/changelog/136309.yaml b/docs/changelog/136309.yaml new file mode 100644 index 0000000000000..316cf300f7933 --- /dev/null +++ b/docs/changelog/136309.yaml @@ -0,0 +1,5 @@ +pr: 136309 +summary: Adds ST_SIMPLIFY spatial funcion +area: ES|QL +type: enhancement +issues: [] From 908632ba3d3f9dd10dae3a397dcf05f530ace2a0 Mon Sep 17 00:00:00 2001 From: ncordon Date: Thu, 9 Oct 2025 18:32:09 +0200 Subject: [PATCH 09/11] Updates changelog --- docs/changelog/136309.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog/136309.yaml b/docs/changelog/136309.yaml index 316cf300f7933..33192872867d1 100644 --- a/docs/changelog/136309.yaml +++ b/docs/changelog/136309.yaml @@ -1,5 +1,6 @@ pr: 136309 -summary: Adds ST_SIMPLIFY spatial funcion +summary: Adds ST_SIMPLIFY geo spatial function area: ES|QL type: enhancement -issues: [] +issues: + - 48521 From dd2933eaf4adde27fde88761d8ae6d0fbea5fe89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nacho=20Cord=C3=B3n?= Date: Thu, 9 Oct 2025 18:32:22 +0200 Subject: [PATCH 10/11] Update docs/changelog/136309.yaml --- docs/changelog/136309.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/changelog/136309.yaml b/docs/changelog/136309.yaml index 33192872867d1..a5a02d4351b68 100644 --- a/docs/changelog/136309.yaml +++ b/docs/changelog/136309.yaml @@ -2,5 +2,4 @@ pr: 136309 summary: Adds ST_SIMPLIFY geo spatial function area: ES|QL type: enhancement -issues: - - 48521 +issues: [] From 6ab6bdcccd0402071a12a74122cad6c3c8c3a791 Mon Sep 17 00:00:00 2001 From: ncordon Date: Fri, 10 Oct 2025 11:36:35 +0200 Subject: [PATCH 11/11] Reshapes the code a bit --- x-pack/plugin/esql-core/build.gradle | 1 + .../licenses/jts-core-LICENSE.txt | 0 .../licenses/jts-core-NOTICE.txt | 0 .../core/util/SpatialCoordinateTypes.java | 16 +++++++++++ x-pack/plugin/esql/build.gradle | 1 - .../function/scalar/spatial/StSimplify.java | 28 +++++++------------ 6 files changed, 27 insertions(+), 19 deletions(-) rename x-pack/plugin/{esql => esql-core}/licenses/jts-core-LICENSE.txt (100%) rename x-pack/plugin/{esql => esql-core}/licenses/jts-core-NOTICE.txt (100%) diff --git a/x-pack/plugin/esql-core/build.gradle b/x-pack/plugin/esql-core/build.gradle index c18c10840d221..eca5ba65ccde5 100644 --- a/x-pack/plugin/esql-core/build.gradle +++ b/x-pack/plugin/esql-core/build.gradle @@ -16,6 +16,7 @@ base { dependencies { api "org.antlr:antlr4-runtime:${versions.antlr4}" api project(path: xpackModule('mapper-version')) + api "org.locationtech.jts:jts-core:${versions.jts}" compileOnly project(path: xpackModule('core')) testApi(project(xpackModule('esql-core:test-fixtures'))) { exclude group: 'org.elasticsearch.plugin', module: 'esql-core' diff --git a/x-pack/plugin/esql/licenses/jts-core-LICENSE.txt b/x-pack/plugin/esql-core/licenses/jts-core-LICENSE.txt similarity index 100% rename from x-pack/plugin/esql/licenses/jts-core-LICENSE.txt rename to x-pack/plugin/esql-core/licenses/jts-core-LICENSE.txt diff --git a/x-pack/plugin/esql/licenses/jts-core-NOTICE.txt b/x-pack/plugin/esql-core/licenses/jts-core-NOTICE.txt similarity index 100% rename from x-pack/plugin/esql/licenses/jts-core-NOTICE.txt rename to x-pack/plugin/esql-core/licenses/jts-core-NOTICE.txt diff --git a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/util/SpatialCoordinateTypes.java b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/util/SpatialCoordinateTypes.java index 019aabda5058e..7662da986b238 100644 --- a/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/util/SpatialCoordinateTypes.java +++ b/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/util/SpatialCoordinateTypes.java @@ -16,6 +16,9 @@ import org.elasticsearch.geometry.utils.GeometryValidator; import org.elasticsearch.geometry.utils.WellKnownBinary; import org.elasticsearch.geometry.utils.WellKnownText; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.io.WKTWriter; import java.nio.ByteOrder; @@ -125,4 +128,17 @@ public String wkbToWkt(BytesRef wkb) { public Geometry wkbToGeometry(BytesRef wkb) { return WellKnownBinary.fromWKB(validator(), false, wkb.bytes, wkb.offset, wkb.length); } + + public org.locationtech.jts.geom.Geometry wkbToJtsGeometry(BytesRef wkb) throws ParseException { + String wkt = wkbToWkt(wkb); + WKTReader reader = new WKTReader(); + return reader.read(wkt); + } + + public BytesRef jtsGeometryToWkb(org.locationtech.jts.geom.Geometry jtsGeometry) { + WKTWriter writer = new WKTWriter(); + String wkt = writer.write(jtsGeometry); + return wktToWkb(wkt); + } + } diff --git a/x-pack/plugin/esql/build.gradle b/x-pack/plugin/esql/build.gradle index 9aeaa789bf9e0..734c0b62eb729 100644 --- a/x-pack/plugin/esql/build.gradle +++ b/x-pack/plugin/esql/build.gradle @@ -37,7 +37,6 @@ dependencies { compileOnly project(xpackModule('ml')) compileOnly project(path: xpackModule('mapper-aggregate-metric')) compileOnly project(path: xpackModule('downsample')) - implementation "org.locationtech.jts:jts-core:${versions.jts}" implementation project(xpackModule('kql')) implementation project('compute') implementation project('compute:ann') diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java index 6fd6f0f253a4d..8f724531ca6e7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -23,8 +23,7 @@ import org.elasticsearch.xpack.esql.expression.function.Param; import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; -import org.locationtech.jts.io.WKTReader; -import org.locationtech.jts.io.WKTWriter; +import org.locationtech.jts.io.ParseException; import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; import java.io.IOException; @@ -93,20 +92,13 @@ public void writeTo(StreamOutput out) throws IOException { out.writeNamedWriteable(tolerance); } - private static class GeoSimplifier { - static WKTReader reader = new WKTReader(); - static WKTWriter writer = new WKTWriter(); - - public static BytesRef geoSourceAndConstantTolerance(BytesRef inputGeometry, @Fixed double inputTolerance) { - String wkt = UNSPECIFIED.wkbToWkt(inputGeometry); - try { - org.locationtech.jts.geom.Geometry jtsGeometry = reader.read(wkt); - org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, inputTolerance); - String simplifiedWkt = writer.write(simplifiedGeometry); - return UNSPECIFIED.wktToWkb(simplifiedWkt); - } catch (Exception e) { - throw new RuntimeException(e); - } + private static BytesRef geoSourceAndConstantTolerance(BytesRef inputGeometry, double inputTolerance) { + try { + org.locationtech.jts.geom.Geometry jtsGeometry = UNSPECIFIED.wkbToJtsGeometry(inputGeometry); + org.locationtech.jts.geom.Geometry simplifiedGeometry = DouglasPeuckerSimplifier.simplify(jtsGeometry, inputTolerance); + return UNSPECIFIED.jtsGeometryToWkb(simplifiedGeometry); + } catch (ParseException e) { + throw new IllegalArgumentException("could not parse the geometry expression: " + e); } } @@ -128,11 +120,11 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua @Evaluator(extraName = "NonFoldableGeoAndConstantTolerance", warnExceptions = { IllegalArgumentException.class }) static BytesRef processNonFoldableGeoAndConstantTolerance(BytesRef inputGeometry, @Fixed double inputTolerance) { - return GeoSimplifier.geoSourceAndConstantTolerance(inputGeometry, inputTolerance); + return geoSourceAndConstantTolerance(inputGeometry, inputTolerance); } @Evaluator(extraName = "FoldableGeoAndConstantTolerance", warnExceptions = { IllegalArgumentException.class }) static BytesRef processFoldableGeoAndConstantTolerance(@Fixed BytesRef inputGeometry, @Fixed double inputTolerance) { - return GeoSimplifier.geoSourceAndConstantTolerance(inputGeometry, inputTolerance); + return geoSourceAndConstantTolerance(inputGeometry, inputTolerance); } }