diff --git a/docs/changelog/136309.yaml b/docs/changelog/136309.yaml new file mode 100644 index 0000000000000..a5a02d4351b68 --- /dev/null +++ b/docs/changelog/136309.yaml @@ -0,0 +1,5 @@ +pr: 136309 +summary: Adds ST_SIMPLIFY geo spatial function +area: ES|QL +type: enhancement +issues: [] 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-core/licenses/jts-core-LICENSE.txt b/x-pack/plugin/esql-core/licenses/jts-core-LICENSE.txt new file mode 100644 index 0000000000000..bc03db03a5926 --- /dev/null +++ b/x-pack/plugin/esql-core/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-core/licenses/jts-core-NOTICE.txt b/x-pack/plugin/esql-core/licenses/jts-core-NOTICE.txt new file mode 100644 index 0000000000000..8d1c8b69c3fce --- /dev/null +++ b/x-pack/plugin/esql-core/licenses/jts-core-NOTICE.txt @@ -0,0 +1 @@ + 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/qa/testFixtures/src/main/resources/spatial.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec index 34e5c2394ff54..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 @@ -3024,3 +3024,123 @@ wkt:keyword |pt:cartesian_point "POINT(111)" |null // end::to_cartesianpoint-str-parse-error-result[] ; + +############################################### +# Tests for ST_SIMPLIFY +############################################### + +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 +; + +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/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 36b61e127888e..84d2f7744e971 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/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index c6520c8563d6d..d24e154da62cd 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 @@ -168,6 +168,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; @@ -443,6 +444,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..8f724531ca6e7 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StSimplify.java @@ -0,0 +1,130 @@ +/* + * 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.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.ann.Evaluator; +import org.elasticsearch.compute.ann.Fixed; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.xpack.esql.core.expression.Expression; +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.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.ParseException; +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.core.util.SpatialCoordinateTypes.UNSPECIFIED; + +public class StSimplify extends EsqlScalarFunction { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( + Expression.class, + "StSimplify", + StSimplify::new + ); + 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; + } + + 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; + } + + @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 ENTRY.name; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + source().writeTo(out); + out.writeNamedWriteable(geometry); + out.writeNamedWriteable(tolerance); + } + + 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); + } + } + + @Override + public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { + 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 geoSourceAndConstantTolerance(inputGeometry, inputTolerance); + } + + @Evaluator(extraName = "FoldableGeoAndConstantTolerance", warnExceptions = { IllegalArgumentException.class }) + static BytesRef processFoldableGeoAndConstantTolerance(@Fixed BytesRef inputGeometry, @Fixed double inputTolerance) { + return geoSourceAndConstantTolerance(inputGeometry, inputTolerance); + } +}