diff --git a/docs/changelog/86154.yaml b/docs/changelog/86154.yaml new file mode 100644 index 0000000000000..eff9316d5f9ce --- /dev/null +++ b/docs/changelog/86154.yaml @@ -0,0 +1,5 @@ +pr: 86154 +summary: Support geo label position as runtime field +area: Geo +type: enhancement +issues: [] diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt index 46c14af040f96..79509e73afa85 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt @@ -85,6 +85,7 @@ class org.elasticsearch.index.fielddata.ScriptDocValues$Geometry { int getDimensionalType() org.elasticsearch.common.geo.GeoPoint getCentroid() org.elasticsearch.common.geo.GeoBoundingBox getBoundingBox() + org.elasticsearch.common.geo.GeoPoint getLabelPosition() double getMercatorWidth() double getMercatorHeight() } diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml index d3b43575b58a4..63f01c4f53b36 100644 --- a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml +++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml @@ -638,6 +638,18 @@ setup: - match: { hits.hits.0.fields.bbox.0.bottom_right.lat: 41.1199999647215 } - match: { hits.hits.0.fields.bbox.0.bottom_right.lon: -71.34000004269183 } + - do: + search: + rest_total_hits_as_int: true + body: + query: { term: { _id: "1" } } + script_fields: + label_position: + script: + source: "doc['geo_point'].getLabelPosition()" + - match: { hits.hits.0.fields.label_position.0.lat: 41.1199999647215 } + - match: { hits.hits.0.fields.label_position.0.lon: -71.34000004269183 } + - do: search: rest_total_hits_as_int: true @@ -661,9 +673,9 @@ setup: body: query: { term: { _id: "1" } } script_fields: - type: - script: - source: "doc['geo_point'].getDimensionalType()" + type: + script: + source: "doc['geo_point'].getDimensionalType()" - match: { hits.hits.0.fields.type.0: 0 } - do: diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPointScriptDocValuesIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPointScriptDocValuesIT.java index 5f7871ec9e478..cbd403ba8c9d0 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPointScriptDocValuesIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoPointScriptDocValuesIT.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.document.DocumentField; import org.elasticsearch.common.geo.GeoBoundingBox; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.geo.GeometryTestUtils; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.plugins.Plugin; @@ -21,6 +22,8 @@ import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; import org.hamcrest.Matchers; import org.junit.Before; @@ -36,6 +39,8 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.oneOf; public class GeoPointScriptDocValuesIT extends ESSingleNodeTestCase { @@ -54,6 +59,8 @@ protected Map, Object>> pluginScripts() { scripts.put("lon", this::scriptLon); scripts.put("height", this::scriptHeight); scripts.put("width", this::scriptWidth); + scripts.put("label_lat", this::scriptLabelLat); + scripts.put("label_lon", this::scriptLabelLon); return scripts; } @@ -91,15 +98,29 @@ private double scriptLon(Map vars) { return geometry.size() == 0 ? Double.NaN : geometry.getCentroid().lon(); } + private double scriptLabelLat(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + return geometry.size() == 0 ? Double.NaN : geometry.getLabelPosition().lat(); + } + + private double scriptLabelLon(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + return geometry.size() == 0 ? Double.NaN : geometry.getLabelPosition().lon(); + } + private ScriptDocValues.Geometry assertGeometry(Map doc) { ScriptDocValues.Geometry geometry = (ScriptDocValues.Geometry) doc.get("location"); if (geometry.size() == 0) { assertThat(geometry.getBoundingBox(), Matchers.nullValue()); assertThat(geometry.getCentroid(), Matchers.nullValue()); + assertThat(geometry.getLabelPosition(), Matchers.nullValue()); assertThat(geometry.getDimensionalType(), equalTo(-1)); } else { assertThat(geometry.getBoundingBox(), Matchers.notNullValue()); assertThat(geometry.getCentroid(), Matchers.notNullValue()); + assertThat(geometry.getLabelPosition(), Matchers.notNullValue()); assertThat(geometry.getDimensionalType(), equalTo(0)); } return geometry; @@ -140,6 +161,8 @@ public void testRandomPoint() throws Exception { .addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap())) .addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap())) .addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap())) + .addScriptField("label_lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lat", Collections.emptyMap())) + .addScriptField("label_lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lon", Collections.emptyMap())) .get(); assertSearchResponse(searchResponse); @@ -151,6 +174,10 @@ public void testRandomPoint() throws Exception { assertThat(fields.get("lon").getValue(), equalTo(qLon)); assertThat(fields.get("height").getValue(), equalTo(0d)); assertThat(fields.get("width").getValue(), equalTo(0d)); + + // Check label position is the same point + assertThat(fields.get("label_lon").getValue(), equalTo(qLon)); + assertThat(fields.get("label_lat").getValue(), equalTo(qLat)); } public void testRandomMultiPoint() throws Exception { @@ -178,6 +205,8 @@ public void testRandomMultiPoint() throws Exception { .addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap())) .addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap())) .addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap())) + .addScriptField("label_lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lat", Collections.emptyMap())) + .addScriptField("label_lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lon", Collections.emptyMap())) .get(); assertSearchResponse(searchResponse); @@ -196,6 +225,11 @@ public void testRandomMultiPoint() throws Exception { assertThat(fields.get("lon").getValue(), equalTo(centroidLon)); assertThat(fields.get("height").getValue(), equalTo(height)); assertThat(fields.get("width").getValue(), equalTo(width)); + + // Check label position is one of the incoming points + double labelLat = fields.get("label_lat").getValue(); + double labelLon = fields.get("label_lon").getValue(); + assertThat("Label should be one of the points", new GeoPoint(labelLat, labelLon), isMultiPointLabelPosition(lats, lons)); } public void testNullPoint() throws Exception { @@ -221,4 +255,29 @@ public void testNullPoint() throws Exception { assertThat(fields.get("height").getValue(), equalTo(Double.NaN)); assertThat(fields.get("width").getValue(), equalTo(Double.NaN)); } + + private static MultiPointLabelPosition isMultiPointLabelPosition(double[] lats, double[] lons) { + return new MultiPointLabelPosition(lats, lons); + } + + private static class MultiPointLabelPosition extends BaseMatcher { + private final GeoPoint[] points; + + private MultiPointLabelPosition(double[] lats, double[] lons) { + points = new GeoPoint[lats.length]; + for (int i = 0; i < lats.length; i++) { + points[i] = new GeoPoint(lats[i], lons[i]); + } + } + + @Override + public boolean matches(Object actual) { + return is(oneOf(points)).matches(actual); + } + + @Override + public void describeTo(Description description) { + description.appendText("is(oneOf(" + Arrays.toString(points) + ")"); + } + } } diff --git a/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java b/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java index f014803038c30..de7320ab07564 100644 --- a/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java +++ b/server/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java @@ -232,6 +232,9 @@ public Geometry(Supplier supplier) { /** Returns the bounding box of this geometry */ public abstract GeoBoundingBox getBoundingBox(); + /** Returns the suggested label position */ + public abstract GeoPoint getLabelPosition(); + /** Returns the centroid of this geometry */ public abstract GeoPoint getCentroid(); @@ -247,6 +250,8 @@ public interface GeometrySupplier extends Supplier { GeoPoint getInternalCentroid(); GeoBoundingBox getInternalBoundingBox(); + + GeoPoint getInternalLabelPosition(); } public static class GeoPoints extends Geometry { @@ -363,6 +368,11 @@ public double getMercatorHeight() { public GeoBoundingBox getBoundingBox() { return size() == 0 ? null : geometrySupplier.getInternalBoundingBox(); } + + @Override + public GeoPoint getLabelPosition() { + return size() == 0 ? null : geometrySupplier.getInternalLabelPosition(); + } } public static class Booleans extends ScriptDocValues { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java index be24a4aa585e1..beb594d9e9936 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java @@ -44,7 +44,12 @@ public interface GeoShapeQueryable { Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, LatLonGeometry... luceneGeometries); default Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, Geometry shape) { - final LatLonGeometry[] luceneGeometries = toQuantizeLuceneGeometry(fieldName, context, shape, relation); + final LatLonGeometry[] luceneGeometries; + try { + luceneGeometries = toQuantizeLuceneGeometry(shape, relation); + } catch (IllegalArgumentException e) { + throw new QueryShardException(context, "Exception creating query on Field [" + fieldName + "] " + e.getMessage(), e); + } if (luceneGeometries.length == 0) { return new MatchNoDocsQuery(); } @@ -82,12 +87,7 @@ private static double[] quantizeLons(double[] lons) { * transforms an Elasticsearch {@link Geometry} into a lucene {@link LatLonGeometry} and quantize * the latitude and longitude values to match the values on the index. */ - private static LatLonGeometry[] toQuantizeLuceneGeometry( - String name, - SearchExecutionContext context, - Geometry geometry, - ShapeRelation relation - ) { + static LatLonGeometry[] toQuantizeLuceneGeometry(Geometry geometry, ShapeRelation relation) { if (geometry == null) { return new LatLonGeometry[0]; } @@ -130,7 +130,7 @@ public Void visit(org.elasticsearch.geometry.Line line) { if (relation == ShapeRelation.WITHIN) { // Line geometries and WITHIN relation is not supported by Lucene. Throw an error here // to have same behavior for runtime fields. - throw new QueryShardException(context, "Field [" + name + "] found an unsupported shape Line"); + throw new IllegalArgumentException("found an unsupported shape Line"); } geometries.add(new org.apache.lucene.geo.Line(quantizeLats(line.getLats()), quantizeLons(line.getLons()))); } @@ -139,7 +139,7 @@ public Void visit(org.elasticsearch.geometry.Line line) { @Override public Void visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + name + "] found an unsupported shape LinearRing"); + throw new IllegalArgumentException("Found an unsupported shape LinearRing"); } @Override diff --git a/server/src/main/java/org/elasticsearch/script/field/GeoPointDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/GeoPointDocValuesField.java index c71d20596235d..d497630fd34d8 100644 --- a/server/src/main/java/org/elasticsearch/script/field/GeoPointDocValuesField.java +++ b/server/src/main/java/org/elasticsearch/script/field/GeoPointDocValuesField.java @@ -11,6 +11,7 @@ import org.apache.lucene.util.ArrayUtil; import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.index.fielddata.MultiGeoPointValues; import org.elasticsearch.index.fielddata.ScriptDocValues; @@ -34,6 +35,7 @@ public class GeoPointDocValuesField extends AbstractScriptFieldFactory private ScriptDocValues.GeoPoints geoPoints = null; private final GeoPoint centroid = new GeoPoint(); private final GeoBoundingBox boundingBox = new GeoBoundingBox(new GeoPoint(), new GeoPoint()); + private int labelIndex = 0; public GeoPointDocValuesField(MultiGeoPointValues input, String name) { this.input = input; @@ -71,11 +73,13 @@ private void setSingleValue() throws IOException { centroid.reset(point.lat(), point.lon()); boundingBox.topLeft().reset(point.lat(), point.lon()); boundingBox.bottomRight().reset(point.lat(), point.lon()); + labelIndex = 0; } private void setMultiValue() throws IOException { double centroidLat = 0; double centroidLon = 0; + labelIndex = 0; double maxLon = Double.NEGATIVE_INFINITY; double minLon = Double.POSITIVE_INFINITY; double maxLat = Double.NEGATIVE_INFINITY; @@ -89,12 +93,22 @@ private void setMultiValue() throws IOException { minLon = Math.min(minLon, values[i].getLon()); maxLat = Math.max(maxLat, values[i].getLat()); minLat = Math.min(minLat, values[i].getLat()); + labelIndex = closestPoint(labelIndex, i, (minLat + maxLat) / 2, (minLon + maxLon) / 2); } centroid.reset(centroidLat / count, centroidLon / count); boundingBox.topLeft().reset(maxLat, minLon); boundingBox.bottomRight().reset(minLat, maxLon); } + private int closestPoint(int a, int b, double lat, double lon) { + if (a == b) { + return a; + } + double distA = GeoUtils.planeDistance(lat, lon, values[a].lat(), values[a].lon()); + double distB = GeoUtils.planeDistance(lat, lon, values[b].lat(), values[b].lon()); + return distA < distB ? a : b; + } + @Override public ScriptDocValues toScriptDocValues() { if (geoPoints == null) { @@ -121,6 +135,11 @@ public GeoBoundingBox getInternalBoundingBox() { return boundingBox; } + @Override + public GeoPoint getInternalLabelPosition() { + return values[labelIndex]; + } + @Override public String getName() { return name; diff --git a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeScriptDocValuesIT.java b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeScriptDocValuesIT.java index 8a999ad914b3b..da831d85f554e 100644 --- a/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeScriptDocValuesIT.java +++ b/x-pack/plugin/spatial/src/internalClusterTest/java/org/elasticsearch/xpack/spatial/search/GeoShapeScriptDocValuesIT.java @@ -11,9 +11,14 @@ import org.elasticsearch.common.geo.GeoBoundingBox; import org.elasticsearch.common.geo.Orientation; import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Line; import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Polygon; +import org.elasticsearch.geometry.utils.GeographyValidator; import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.index.mapper.GeoShapeIndexer; @@ -26,12 +31,15 @@ import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.spatial.LocalStateSpatialPlugin; +import org.elasticsearch.xpack.spatial.index.fielddata.DimensionalShapeType; import org.elasticsearch.xpack.spatial.index.fielddata.GeoShapeValues; import org.elasticsearch.xpack.spatial.util.GeoTestUtils; import org.hamcrest.Matchers; import org.junit.Before; import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -63,6 +71,8 @@ protected Map, Object>> pluginScripts() { scripts.put("lon", this::scriptLon); scripts.put("height", this::scriptHeight); scripts.put("width", this::scriptWidth); + scripts.put("label_lat", this::scriptLabelLat); + scripts.put("label_lon", this::scriptLabelLon); return scripts; } @@ -100,15 +110,29 @@ private double scriptLon(Map vars) { return geometry.size() == 0 ? Double.NaN : geometry.getCentroid().lon(); } + private double scriptLabelLat(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + return geometry.size() == 0 ? Double.NaN : geometry.getLabelPosition().lat(); + } + + private double scriptLabelLon(Map vars) { + Map doc = (Map) vars.get("doc"); + ScriptDocValues.Geometry geometry = assertGeometry(doc); + return geometry.size() == 0 ? Double.NaN : geometry.getLabelPosition().lon(); + } + private ScriptDocValues.Geometry assertGeometry(Map doc) { ScriptDocValues.Geometry geometry = (ScriptDocValues.Geometry) doc.get("location"); if (geometry.size() == 0) { assertThat(geometry.getBoundingBox(), Matchers.nullValue()); assertThat(geometry.getCentroid(), Matchers.nullValue()); + assertThat(geometry.getLabelPosition(), Matchers.nullValue()); assertThat(geometry.getDimensionalType(), equalTo(-1)); } else { assertThat(geometry.getBoundingBox(), Matchers.notNullValue()); assertThat(geometry.getCentroid(), Matchers.notNullValue()); + assertThat(geometry.getLabelPosition(), Matchers.notNullValue()); assertThat(geometry.getDimensionalType(), greaterThanOrEqualTo(0)); assertThat(geometry.getDimensionalType(), lessThanOrEqualTo(2)); } @@ -147,12 +171,79 @@ public void testRandomShape() throws Exception { doTestGeometry(geometry); } + public void testPolygonFromYamlTests() throws IOException, ParseException { + // This is the geometry used in the tests in 70_script_doc_values.yml, and is easier to test and debug here + String wkt = "POLYGON((" + + "24.04725 59.942,24.04825 59.94125,24.04875 59.94125,24.04875 59.94175,24.048 59.9425," + + "24.0475 59.94275,24.0465 59.94225,24.046 59.94225,24.04575 59.9425,24.04525 59.94225,24.04725 59.942" + + "))"; + Geometry polygon = WellKnownText.fromWKT(GeographyValidator.instance(true), true, wkt); + doTestGeometry(polygon, null); + } + public void testPolygonDateline() throws Exception { Geometry geometry = new Polygon(new LinearRing(new double[] { 170, 190, 190, 170, 170 }, new double[] { -5, -5, 5, 5, -5 })); - doTestGeometry(geometry); + doTestGeometry(geometry, null); + } + + private MultiPoint pointsFromLine(Line line) { + ArrayList points = new ArrayList<>(); + for (int i = 0; i < line.length(); i++) { + double x = line.getX(i); + double y = line.getY(i); + points.add(new Point(x, y)); + } + return new MultiPoint(points); + } + + public void testEvenLineString() throws Exception { + Line line = new Line(new double[] { -5, -1, 0, 1, 5 }, new double[] { 0, 0, 0, 0, 0 }); + doTestGeometry(line, GeoTestUtils.geoShapeValue(new Point(-0.5, 0))); + doTestGeometry(pointsFromLine(line), GeoTestUtils.geoShapeValue(new Point(0, 0))); + } + + public void testOddLineString() throws Exception { + Line line = new Line(new double[] { -5, -1, 1, 5 }, new double[] { 0, 0, 0, 0 }); + doTestGeometry(line, GeoTestUtils.geoShapeValue(new Point(0, 0))); + doTestGeometry(pointsFromLine(line), GeoTestUtils.geoShapeValue(new Point(-1, 0))); + } + + public void testUnbalancedEvenLineString() throws Exception { + Line line = new Line(new double[] { -5, -4, -3, -2, -1, 0, 5 }, new double[] { 0, 0, 0, 0, 0, 0, 0 }); + doTestGeometry(line, GeoTestUtils.geoShapeValue(new Point(-2.5, 0))); + doTestGeometry(pointsFromLine(line), GeoTestUtils.geoShapeValue(new Point(-2, 0))); + } + + public void testUnbalancedOddLineString() throws Exception { + Line line = new Line(new double[] { -5, -4, -3, -2, -1, 5 }, new double[] { 0, 0, 0, 0, 0, 0 }); + doTestGeometry(line, GeoTestUtils.geoShapeValue(new Point(-2.5, 0))); + doTestGeometry(pointsFromLine(line), GeoTestUtils.geoShapeValue(new Point(-3, 0))); + } + + public void testVerticalLineString() throws Exception { + // Data with no x-range is not well sorted and odd choices occur for the first triangle tree node + Line line = new Line(new double[] { 0, 0, 0, 0, 0 }, new double[] { -5, -1, 0, 1, 5 }); + doTestGeometry(line, GeoTestUtils.geoShapeValue(new Point(0, 3))); + doTestGeometry(pointsFromLine(line), GeoTestUtils.geoShapeValue(new Point(0, 1))); + } + + public void testOffVerticalLineString() throws Exception { + // Even a very small x-range results in reasonable sorting for the label position + Line line = new Line(new double[] { -0.0005, -0.0001, 0, 0.0001, 0.0005 }, new double[] { -5, -1, 0, 1, 5 }); + doTestGeometry(line, GeoTestUtils.geoShapeValue(new Point(-0.00005, -0.5))); + doTestGeometry(pointsFromLine(line), GeoTestUtils.geoShapeValue(new Point(0, 0))); } private void doTestGeometry(Geometry geometry) throws IOException { + doTestGeometry(geometry, null, false); + } + + private void doTestGeometry(Geometry geometry, GeoShapeValues.GeoShapeValue expectedLabelPosition) throws IOException { + doTestGeometry(geometry, expectedLabelPosition, true); + } + + private void doTestGeometry(Geometry geometry, GeoShapeValues.GeoShapeValue expectedLabelPosition, boolean fallbackToCentroid) + throws IOException { client().prepareIndex("test") .setId("1") .setSource( @@ -170,6 +261,8 @@ private void doTestGeometry(Geometry geometry) throws IOException { .addScriptField("lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "lon", Collections.emptyMap())) .addScriptField("height", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "height", Collections.emptyMap())) .addScriptField("width", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "width", Collections.emptyMap())) + .addScriptField("label_lat", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lat", Collections.emptyMap())) + .addScriptField("label_lon", new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "label_lon", Collections.emptyMap())) .get(); assertSearchResponse(searchResponse); Map fields = searchResponse.getHits().getHits()[0].getFields(); @@ -177,6 +270,30 @@ private void doTestGeometry(Geometry geometry) throws IOException { assertThat(fields.get("lon").getValue(), equalTo(value.lon())); assertThat(fields.get("height").getValue(), equalTo(value.boundingBox().maxY() - value.boundingBox().minY())); assertThat(fields.get("width").getValue(), equalTo(value.boundingBox().maxX() - value.boundingBox().minX())); + + // Check label position is in the geometry, but with a tolerance constructed as a circle of 1m radius to handle quantization + Point labelPosition = new Point(fields.get("label_lon").getValue(), fields.get("label_lat").getValue()); + Circle tolerance = new Circle(labelPosition.getX(), labelPosition.getY(), 1); + assertTrue("Expect label position " + labelPosition + " to intersect geometry " + geometry, value.intersects(tolerance)); + + // Check that the label position is the expected one, or the centroid in certain polygon cases + if (expectedLabelPosition != null) { + doTestLabelPosition(fields, expectedLabelPosition); + } else if (fallbackToCentroid && value.dimensionalShapeType() == DimensionalShapeType.POLYGON) { + // Use the centroid for all polygons, unless overwritten for specific cases + doTestLabelPosition(fields, GeoTestUtils.geoShapeValue(new Point(value.lon(), value.lat()))); + } + } + + private void doTestLabelPosition(Map fields, GeoShapeValues.GeoShapeValue expectedLabelPosition) + throws IOException { + assertEquals("Unexpected latitude for label position,", expectedLabelPosition.lat(), fields.get("label_lat").getValue(), 0.0000001); + assertEquals( + "Unexpected longitude for label position,", + expectedLabelPosition.lon(), + fields.get("label_lon").getValue(), + 0.0000001 + ); } public void testNullShape() throws Exception { diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java index 781a062c4b065..003a941424b23 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/GeoShapeValues.java @@ -7,13 +7,19 @@ package org.elasticsearch.xpack.spatial.index.fielddata; +import org.apache.lucene.document.ShapeField; +import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.Orientation; +import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.geometry.utils.GeographyValidator; import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.index.mapper.GeoShapeIndexer; +import org.elasticsearch.index.mapper.GeoShapeQueryable; import org.elasticsearch.search.aggregations.support.ValuesSourceType; import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContentBuilder; @@ -107,6 +113,20 @@ public BoundingBox boundingBox() { return boundingBox; } + /** + * Select a label position that is within the shape. + */ + public GeoPoint labelPosition() throws IOException { + // For polygons we prefer to use the centroid, as long as it is within the polygon + if (reader.getDimensionalShapeType() == DimensionalShapeType.POLYGON && intersects(new Point(lon(), lat()))) { + return new GeoPoint(lat(), lon()); + } + // For all other cases, use the first triangle (or line or point) in the tree which will always intersect the shape + LabelPositionVisitor visitor = new LabelPositionVisitor(CoordinateEncoder.GEO); + reader.visit(visitor); + return visitor.labelPosition(); + } + public GeoRelation relate(Rectangle rectangle) throws IOException { int minX = CoordinateEncoder.GEO.encodeX(rectangle.getMinX()); int maxX = CoordinateEncoder.GEO.encodeX(rectangle.getMaxX()); @@ -117,6 +137,24 @@ public GeoRelation relate(Rectangle rectangle) throws IOException { return tile2DVisitor.relation(); } + /** + * Determine if the current shape value intersects the specified geometry. + * Note that the intersection must be true in quantized space, so it is possible that + * points on the edges of geometries will return false due to quantization shifting them off the geometry. + * To deal with this, one option is to pass in a circle around the point with a 1m radius + * which is enough to cover the resolution of the quantization. + */ + public boolean intersects(Geometry geometry) throws IOException { + LatLonGeometry[] latLonGeometries = GeoShapeQueryable.toQuantizeLuceneGeometry(geometry, ShapeRelation.INTERSECTS); + Component2DVisitor visitor = Component2DVisitor.getVisitor( + LatLonGeometry.create(latLonGeometries), + ShapeField.QueryRelation.INTERSECTS, + CoordinateEncoder.GEO + ); + reader.visit(visitor); + return visitor.matches(); + } + public DimensionalShapeType dimensionalShapeType() { return reader.getDimensionalShapeType(); } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/LabelPositionVisitor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/LabelPositionVisitor.java new file mode 100644 index 0000000000000..a8ba38528db46 --- /dev/null +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/LabelPositionVisitor.java @@ -0,0 +1,93 @@ +/* + * 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.spatial.index.fielddata; + +import org.elasticsearch.common.geo.GeoPoint; + +/** + * Get the first node of the tree and provide a point in that gemetry (point, line or triangle) + * as a suggested label position likely to be somewhere in the middle of the entire geometry. + * + * TODO: We could instead choose the point closer to the centroid which improves unbalanced trees + */ +public class LabelPositionVisitor implements TriangleTreeReader.Visitor { + + private GeoPoint labelPosition; + private final CoordinateEncoder encoder; + + public LabelPositionVisitor(CoordinateEncoder encoder) { + this.encoder = encoder; + } + + @Override + public void visitPoint(int x, int y) { + double lon = encoder.decodeX(x); + double lat = encoder.decodeY(y); + // System.out.println("Got point: (" + lon + "," + lat + ")"); + assert labelPosition == null; + labelPosition = new GeoPoint(lat, lon); + } + + @Override + public void visitLine(int aX, int aY, int bX, int bY, byte metadata) { + double aLon = encoder.decodeX(aX); + double aLat = encoder.decodeY(aY); + double bLon = encoder.decodeX(bX); + double bLat = encoder.decodeY(bY); + // System.out.println("Got line: (" + aLon + "," + aLat + ")-(" + bLon + "," + bLat + ")"); + assert labelPosition == null; + labelPosition = new GeoPoint((aLat + bLat) / 2.0, (aLon + bLon) / 2.0); + } + + @Override + public void visitTriangle(int aX, int aY, int bX, int bY, int cX, int cY, byte metadata) { + double aLon = encoder.decodeX(aX); + double aLat = encoder.decodeY(aY); + double bLon = encoder.decodeX(bX); + double bLat = encoder.decodeY(bY); + double cLon = encoder.decodeX(cX); + double cLat = encoder.decodeY(cY); + // System.out.println("Got triangle: (" + aLon + "," + aLat + ")-(" + bLon + "," + bLat + ")-(" + cLon + "," + cLat + ")"); + assert labelPosition == null; + labelPosition = new GeoPoint((aLat + bLat + cLat) / 3.0, (aLon + bLon + cLon) / 3.0); + } + + @Override + public boolean push() { + // Don't traverse deeper once we found a result + return labelPosition == null; + } + + @Override + public boolean pushX(int minX) { + // Don't traverse deeper once we found a result + return labelPosition == null; + } + + @Override + public boolean pushY(int minY) { + // Don't traverse deeper once we found a result + return labelPosition == null; + } + + @Override + public boolean push(int maxX, int maxY) { + // Don't traverse deeper once we found a result + return labelPosition == null; + } + + @Override + public boolean push(int minX, int minY, int maxX, int maxY) { + // Always start the traversal + return true; + } + + public GeoPoint labelPosition() { + return labelPosition; + } +} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/AbstractAtomicGeoShapeShapeFieldData.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/AbstractAtomicGeoShapeShapeFieldData.java index 2f6bdbc52c031..9e2e4ed8669d7 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/AbstractAtomicGeoShapeShapeFieldData.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/fielddata/plain/AbstractAtomicGeoShapeShapeFieldData.java @@ -99,6 +99,11 @@ public GeoBoundingBox getBoundingBox() { return gsSupplier.getInternal(0) == null ? null : gsSupplier.getInternalBoundingBox(); } + @Override + public GeoPoint getLabelPosition() { + return gsSupplier.getInternal(0) == null ? null : gsSupplier.getInternalLabelPosition(); + } + @Override public GeoShapeValues.GeoShapeValue get(int index) { return gsSupplier.getInternal(0); diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapper.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapper.java index e96350da917d5..12637f926766f 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapper.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapper.java @@ -50,6 +50,7 @@ import org.elasticsearch.xpack.spatial.search.aggregations.support.GeoShapeValuesSourceType; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -401,6 +402,15 @@ public GeoBoundingBox getInternalBoundingBox() { return boundingBox; } + @Override + public GeoPoint getInternalLabelPosition() { + try { + return value.labelPosition(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to parse geo shape label position: " + e.getMessage(), e); + } + } + @Override public String getName() { return name; diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueTests.java index 79d88c252f8ee..1026f2eaa246d 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/fielddata/GeometryDocValueTests.java @@ -7,18 +7,34 @@ package org.elasticsearch.xpack.spatial.index.fielddata; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeometryNormalizer; import org.elasticsearch.common.geo.Orientation; +import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.GeometryCollection; +import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiPolygon; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.geometry.utils.StandardValidator; +import org.elasticsearch.geometry.utils.WellKnownText; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.spatial.util.GeoTestUtils; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.zip.GZIPInputStream; import static org.elasticsearch.geo.GeometryTestUtils.randomLine; import static org.elasticsearch.geo.GeometryTestUtils.randomMultiLine; @@ -27,6 +43,9 @@ import static org.elasticsearch.geo.GeometryTestUtils.randomPoint; import static org.elasticsearch.geo.GeometryTestUtils.randomPolygon; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.oneOf; public class GeometryDocValueTests extends ESTestCase { @@ -101,16 +120,151 @@ public void testRectangleShape() throws IOException { int maxX = randomIntBetween(1, 40); int minY = randomIntBetween(-40, -1); int maxY = randomIntBetween(1, 40); - Geometry rectangle = new Rectangle(minX, maxX, maxY, minY); + Rectangle rectangle = new Rectangle(minX, maxX, maxY, minY); GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(rectangle, CoordinateEncoder.GEO); Extent expectedExtent = getExtentFromBox(minX, minY, maxX, maxY); - assertThat(expectedExtent, equalTo(reader.getExtent())); + assertThat("Rectangle extent", reader.getExtent(), equalTo(expectedExtent)); // centroid is calculated using original double values but then loses precision as it is serialized as an integer int encodedCentroidX = CoordinateEncoder.GEO.encodeX(((double) minX + maxX) / 2); int encodedCentroidY = CoordinateEncoder.GEO.encodeY(((double) minY + maxY) / 2); assertEquals(encodedCentroidX, reader.getCentroidX()); assertEquals(encodedCentroidY, reader.getCentroidY()); + + // Label position is the centroid if within the polygon + GeoShapeValues.GeoShapeValue shapeValue = GeoTestUtils.geoShapeValue(rectangle); + GeoPoint labelPosition = shapeValue.labelPosition(); + double labelLon = ((double) minX + maxX) / 2; + double labelLat = ((double) minY + maxY) / 2; + assertEquals(labelLon, labelPosition.lon(), 0.0000001); + assertEquals(labelLat, labelPosition.lat(), 0.0000001); + } + } + + public void testNonCentroidPolygon() throws IOException { + final Rectangle r1 = new Rectangle(-10, -5, 10, -10); + final Rectangle r2 = new Rectangle(5, 10, 10, -10); + MultiPolygon geometry = new MultiPolygon(List.of(toPolygon(r1), toPolygon(r2))); + GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(geometry, CoordinateEncoder.GEO); + + // Centroid is at the origin + int encodedCentroidX = CoordinateEncoder.GEO.encodeX(0); + int encodedCentroidY = CoordinateEncoder.GEO.encodeY(0); + assertEquals(encodedCentroidX, reader.getCentroidX()); + assertEquals(encodedCentroidY, reader.getCentroidY()); + + // Label position is calculated as the first triangle + GeoShapeValues.GeoShapeValue shapeValue = GeoTestUtils.geoShapeValue(geometry); + GeoPoint labelPosition = shapeValue.labelPosition(); + assertThat( + "Expect label position to match one of eight triangles in the two rectangles", + labelPosition, + isRectangleLabelPosition(r1, r2) + ); + } + + public void testAntarcticaLabelPosition() throws Exception { + Geometry geometry = loadResourceAsGeometry("Antarctica.wkt.gz"); + GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(geometry, CoordinateEncoder.GEO); + + // Centroid is near the South Pole + int encodedLatThreshold = CoordinateEncoder.GEO.encodeY(-80); + assertThat( + "Centroid should be near the South Pole, or further South than -80 degrees", + reader.getCentroidY(), + lessThan(encodedLatThreshold) + ); + + // Label position is the centroid if within the polygon + GeoShapeValues.GeoShapeValue shapeValue = GeoTestUtils.geoShapeValue(geometry); + GeoPoint labelPosition = shapeValue.labelPosition(); + double centroidX = CoordinateEncoder.GEO.decodeX(reader.getCentroidX()); + double centroidY = CoordinateEncoder.GEO.decodeY(reader.getCentroidY()); + assertEquals(centroidX, labelPosition.lon(), 0.0000001); + assertEquals(centroidY, labelPosition.lat(), 0.0000001); + Circle tolerance = new Circle(centroidX, centroidY, 1); + assertTrue("Expect label position to be within the geometry", shapeValue.intersects(tolerance)); + } + + public void testFranceLabelPosition() throws Exception { + Geometry geometry = loadResourceAsGeometry("France.wkt.gz"); + GeometryDocValueReader reader = GeoTestUtils.geometryDocValueReader(geometry, CoordinateEncoder.GEO); + + // Label position is the centroid if within the polygon + GeoShapeValues.GeoShapeValue shapeValue = GeoTestUtils.geoShapeValue(geometry); + GeoPoint labelPosition = shapeValue.labelPosition(); + double centroidX = CoordinateEncoder.GEO.decodeX(reader.getCentroidX()); + double centroidY = CoordinateEncoder.GEO.decodeY(reader.getCentroidY()); + assertEquals(centroidX, labelPosition.lon(), 0.0000001); + assertEquals(centroidY, labelPosition.lat(), 0.0000001); + Circle tolerance = new Circle(centroidX, centroidY, 1); + assertTrue("Expect label position to be within the geometry", shapeValue.intersects(tolerance)); + } + + private Geometry loadResourceAsGeometry(String filename) throws IOException, ParseException { + GZIPInputStream is = new GZIPInputStream(getClass().getResourceAsStream(filename)); + final BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + final Geometry geometry = WellKnownText.fromWKT(StandardValidator.instance(true), true, reader.readLine()); + return geometry; + } + + private Polygon toPolygon(Rectangle r) { + return new Polygon( + new LinearRing( + new double[] { r.getMinX(), r.getMaxX(), r.getMaxX(), r.getMinX(), r.getMinX() }, + new double[] { r.getMinY(), r.getMinY(), r.getMaxY(), r.getMaxY(), r.getMinY() } + ) + ); + } + + private static RectangleLabelPosition isRectangleLabelPosition(Rectangle... rectangles) { + return new RectangleLabelPosition(rectangles); + } + + private static class RectangleLabelPosition extends BaseMatcher { + private final Point[] encodedPositions; + + private RectangleLabelPosition(Rectangle... rectangles) { + encodedPositions = new Point[rectangles.length * 4]; + for (int i = 0; i < rectangles.length; i++) { + Rectangle rectangle = rectangles[i]; + GeoPoint a = new GeoPoint(rectangle.getMinY(), rectangle.getMinX()); + GeoPoint b = new GeoPoint(rectangle.getMinY(), rectangle.getMaxX()); + GeoPoint c = new GeoPoint(rectangle.getMaxY(), rectangle.getMaxX()); + GeoPoint d = new GeoPoint(rectangle.getMaxY(), rectangle.getMinX()); + encodedPositions[i * 4 + 0] = average(a, b, c); + encodedPositions[i * 4 + 1] = average(b, c, d); + encodedPositions[i * 4 + 2] = average(c, d, a); + encodedPositions[i * 4 + 3] = average(d, a, b); + } + } + + private Point average(GeoPoint... points) { + double lon = 0; + double lat = 0; + for (GeoPoint point : points) { + lon += point.lon(); + lat += point.lat(); + } + int x = CoordinateEncoder.GEO.encodeX(lon / points.length); + int y = CoordinateEncoder.GEO.encodeY(lat / points.length); + return new Point(x, y); + } + + @Override + public boolean matches(Object actual) { + if (actual instanceof GeoPoint) { + GeoPoint point = (GeoPoint) actual; + int x = CoordinateEncoder.GEO.encodeX(point.lon()); + int y = CoordinateEncoder.GEO.encodeY(point.lat()); + return is(oneOf(encodedPositions)).matches(new Point(x, y)); + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("is(oneOf(" + Arrays.toString(encodedPositions) + ")"); } } diff --git a/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/Antarctica.wkt.gz b/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/Antarctica.wkt.gz new file mode 100644 index 0000000000000..c105fdae9a14e Binary files /dev/null and b/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/Antarctica.wkt.gz differ diff --git a/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/France.wkt.gz b/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/France.wkt.gz new file mode 100644 index 0000000000000..c97e98015e792 Binary files /dev/null and b/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/France.wkt.gz differ diff --git a/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/LICENSE.txt b/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/LICENSE.txt new file mode 100644 index 0000000000000..43c8963d276d6 --- /dev/null +++ b/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/LICENSE.txt @@ -0,0 +1,540 @@ +## ODC Open Database License (ODbL) + +### Preamble + +The Open Database License (ODbL) is a license agreement intended to +allow users to freely share, modify, and use this Database while +maintaining this same freedom for others. Many databases are covered by +copyright, and therefore this document licenses these rights. Some +jurisdictions, mainly in the European Union, have specific rights that +cover databases, and so the ODbL addresses these rights, too. Finally, +the ODbL is also an agreement in contract for users of this Database to +act in certain ways in return for accessing this Database. + +Databases can contain a wide variety of types of content (images, +audiovisual material, and sounds all in the same database, for example), +and so the ODbL only governs the rights over the Database, and not the +contents of the Database individually. Licensors should use the ODbL +together with another license for the contents, if the contents have a +single set of rights that uniformly covers all of the contents. If the +contents have multiple sets of different rights, Licensors should +describe what rights govern what contents together in the individual +record or in some other way that clarifies what rights apply. + +Sometimes the contents of a database, or the database itself, can be +covered by other rights not addressed here (such as private contracts, +trade mark over the name, or privacy rights / data protection rights +over information in the contents), and so you are advised that you may +have to consult other documents or clear other rights before doing +activities not covered by this License. + +------ + +The Licensor (as defined below) + +and + +You (as defined below) + +agree as follows: + +### 1.0 Definitions of Capitalised Words + +"Collective Database" - Means this Database in unmodified form as part +of a collection of independent databases in themselves that together are +assembled into a collective whole. A work that constitutes a Collective +Database will not be considered a Derivative Database. + +"Convey" - As a verb, means Using the Database, a Derivative Database, +or the Database as part of a Collective Database in any way that enables +a Person to make or receive copies of the Database or a Derivative +Database. Conveying does not include interaction with a user through a +computer network, or creating and Using a Produced Work, where no +transfer of a copy of the Database or a Derivative Database occurs. +"Contents" - The contents of this Database, which includes the +information, independent works, or other material collected into the +Database. For example, the contents of the Database could be factual +data or works such as images, audiovisual material, text, or sounds. + +"Database" - A collection of material (the Contents) arranged in a +systematic or methodical way and individually accessible by electronic +or other means offered under the terms of this License. + +"Database Directive" - Means Directive 96/9/EC of the European +Parliament and of the Council of 11 March 1996 on the legal protection +of databases, as amended or succeeded. + +"Database Right" - Means rights resulting from the Chapter III ("sui +generis") rights in the Database Directive (as amended and as transposed +by member states), which includes the Extraction and Re-utilisation of +the whole or a Substantial part of the Contents, as well as any similar +rights available in the relevant jurisdiction under Section 10.4. + +"Derivative Database" - Means a database based upon the Database, and +includes any translation, adaptation, arrangement, modification, or any +other alteration of the Database or of a Substantial part of the +Contents. This includes, but is not limited to, Extracting or +Re-utilising the whole or a Substantial part of the Contents in a new +Database. + +"Extraction" - Means the permanent or temporary transfer of all or a +Substantial part of the Contents to another medium by any means or in +any form. + +"License" - Means this license agreement and is both a license of rights +such as copyright and Database Rights and an agreement in contract. + +"Licensor" - Means the Person that offers the Database under the terms +of this License. + +"Person" - Means a natural or legal person or a body of persons +corporate or incorporate. + +"Produced Work" - a work (such as an image, audiovisual material, text, +or sounds) resulting from using the whole or a Substantial part of the +Contents (via a search or other query) from this Database, a Derivative +Database, or this Database as part of a Collective Database. + +"Publicly" - means to Persons other than You or under Your control by +either more than 50% ownership or by the power to direct their +activities (such as contracting with an independent consultant). + +"Re-utilisation" - means any form of making available to the public all +or a Substantial part of the Contents by the distribution of copies, by +renting, by online or other forms of transmission. + +"Substantial" - Means substantial in terms of quantity or quality or a +combination of both. The repeated and systematic Extraction or +Re-utilisation of insubstantial parts of the Contents may amount to the +Extraction or Re-utilisation of a Substantial part of the Contents. + +"Use" - As a verb, means doing any act that is restricted by copyright +or Database Rights whether in the original medium or any other; and +includes without limitation distributing, copying, publicly performing, +publicly displaying, and preparing derivative works of the Database, as +well as modifying the Database as may be technically necessary to use it +in a different mode or format. + +"You" - Means a Person exercising rights under this License who has not +previously violated the terms of this License with respect to the +Database, or who has received express permission from the Licensor to +exercise rights under this License despite a previous violation. + +Words in the singular include the plural and vice versa. + +### 2.0 What this License covers + +2.1. Legal effect of this document. This License is: + + a. A license of applicable copyright and neighbouring rights; + + b. A license of the Database Right; and + + c. An agreement in contract between You and the Licensor. + +2.2 Legal rights covered. This License covers the legal rights in the +Database, including: + + a. Copyright. Any copyright or neighbouring rights in the Database. + The copyright licensed includes any individual elements of the + Database, but does not cover the copyright over the Contents + independent of this Database. See Section 2.4 for details. Copyright + law varies between jurisdictions, but is likely to cover: the Database + model or schema, which is the structure, arrangement, and organisation + of the Database, and can also include the Database tables and table + indexes; the data entry and output sheets; and the Field names of + Contents stored in the Database; + + b. Database Rights. Database Rights only extend to the Extraction and + Re-utilisation of the whole or a Substantial part of the Contents. + Database Rights can apply even when there is no copyright over the + Database. Database Rights can also apply when the Contents are removed + from the Database and are selected and arranged in a way that would + not infringe any applicable copyright; and + + c. Contract. This is an agreement between You and the Licensor for + access to the Database. In return you agree to certain conditions of + use on this access as outlined in this License. + +2.3 Rights not covered. + + a. This License does not apply to computer programs used in the making + or operation of the Database; + + b. This License does not cover any patents over the Contents or the + Database; and + + c. This License does not cover any trademarks associated with the + Database. + +2.4 Relationship to Contents in the Database. The individual items of +the Contents contained in this Database may be covered by other rights, +including copyright, patent, data protection, privacy, or personality +rights, and this License does not cover any rights (other than Database +Rights or in contract) in individual Contents contained in the Database. +For example, if used on a Database of images (the Contents), this +License would not apply to copyright over individual images, which could +have their own separate licenses, or one single license covering all of +the rights over the images. + +### 3.0 Rights granted + +3.1 Subject to the terms and conditions of this License, the Licensor +grants to You a worldwide, royalty-free, non-exclusive, terminable (but +only under Section 9) license to Use the Database for the duration of +any applicable copyright and Database Rights. These rights explicitly +include commercial use, and do not exclude any field of endeavour. To +the extent possible in the relevant jurisdiction, these rights may be +exercised in all media and formats whether now known or created in the +future. + +The rights granted cover, for example: + + a. Extraction and Re-utilisation of the whole or a Substantial part of + the Contents; + + b. Creation of Derivative Databases; + + c. Creation of Collective Databases; + + d. Creation of temporary or permanent reproductions by any means and + in any form, in whole or in part, including of any Derivative + Databases or as a part of Collective Databases; and + + e. Distribution, communication, display, lending, making available, or + performance to the public by any means and in any form, in whole or in + part, including of any Derivative Database or as a part of Collective + Databases. + +3.2 Compulsory license schemes. For the avoidance of doubt: + + a. Non-waivable compulsory license schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor reserves + the exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; + + b. Waivable compulsory license schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You of + the rights granted under this License; and, + + c. Voluntary license schemes. The Licensor waives the right to collect + royalties, whether individually or, in the event that the Licensor is + a member of a collecting society that administers voluntary licensing + schemes, via that society, from any exercise by You of the rights + granted under this License. + +3.3 The right to release the Database under different terms, or to stop +distributing or making available the Database, is reserved. Note that +this Database may be multiple-licensed, and so You may have the choice +of using alternative licenses for this Database. Subject to Section +10.4, all other rights not expressly granted by Licensor are reserved. + +### 4.0 Conditions of Use + +4.1 The rights granted in Section 3 above are expressly made subject to +Your complying with the following conditions of use. These are important +conditions of this License, and if You fail to follow them, You will be +in material breach of its terms. + +4.2 Notices. If You Publicly Convey this Database, any Derivative +Database, or the Database as part of a Collective Database, then You +must: + + a. Do so only under the terms of this License or another license + permitted under Section 4.4; + + b. Include a copy of this License (or, as applicable, a license + permitted under Section 4.4) or its Uniform Resource Identifier (URI) + with the Database or Derivative Database, including both in the + Database or Derivative Database and in any relevant documentation; and + + c. Keep intact any copyright or Database Right notices and notices + that refer to this License. + + d. If it is not possible to put the required notices in a particular + file due to its structure, then You must include the notices in a + location (such as a relevant directory) where users would be likely to + look for it. + +4.3 Notice for using output (Contents). Creating and Using a Produced +Work does not require the notice in Section 4.2. However, if you +Publicly Use a Produced Work, You must include a notice associated with +the Produced Work reasonably calculated to make any Person that uses, +views, accesses, interacts with, or is otherwise exposed to the Produced +Work aware that Content was obtained from the Database, Derivative +Database, or the Database as part of a Collective Database, and that it +is available under this License. + + a. Example notice. The following text will satisfy notice under + Section 4.3: + + Contains information from DATABASE NAME, which is made available + here under the Open Database License (ODbL). + +DATABASE NAME should be replaced with the name of the Database and a +hyperlink to the URI of the Database. "Open Database License" should +contain a hyperlink to the URI of the text of this License. If +hyperlinks are not possible, You should include the plain text of the +required URI's with the above notice. + +4.4 Share alike. + + a. Any Derivative Database that You Publicly Use must be only under + the terms of: + + i. This License; + + ii. A later version of this License similar in spirit to this + License; or + + iii. A compatible license. + + If You license the Derivative Database under one of the licenses + mentioned in (iii), You must comply with the terms of that license. + + b. For the avoidance of doubt, Extraction or Re-utilisation of the + whole or a Substantial part of the Contents into a new database is a + Derivative Database and must comply with Section 4.4. + + c. Derivative Databases and Produced Works. A Derivative Database is + Publicly Used and so must comply with Section 4.4. if a Produced Work + created from the Derivative Database is Publicly Used. + + d. Share Alike and additional Contents. For the avoidance of doubt, + You must not add Contents to Derivative Databases under Section 4.4 a + that are incompatible with the rights granted under this License. + + e. Compatible licenses. Licensors may authorise a proxy to determine + compatible licenses under Section 4.4 a iii. If they do so, the + authorised proxy's public statement of acceptance of a compatible + license grants You permission to use the compatible license. + + +4.5 Limits of Share Alike. The requirements of Section 4.4 do not apply +in the following: + + a. For the avoidance of doubt, You are not required to license + Collective Databases under this License if You incorporate this + Database or a Derivative Database in the collection, but this License + still applies to this Database or a Derivative Database as a part of + the Collective Database; + + b. Using this Database, a Derivative Database, or this Database as + part of a Collective Database to create a Produced Work does not + create a Derivative Database for purposes of Section 4.4; and + + c. Use of a Derivative Database internally within an organisation is + not to the public and therefore does not fall under the requirements + of Section 4.4. + +4.6 Access to Derivative Databases. If You Publicly Use a Derivative +Database or a Produced Work from a Derivative Database, You must also +offer to recipients of the Derivative Database or Produced Work a copy +in a machine readable form of: + + a. The entire Derivative Database; or + + b. A file containing all of the alterations made to the Database or + the method of making the alterations to the Database (such as an + algorithm), including any additional Contents, that make up all the + differences between the Database and the Derivative Database. + +The Derivative Database (under a.) or alteration file (under b.) must be +available at no more than a reasonable production cost for physical +distributions and free of charge if distributed over the internet. + +4.7 Technological measures and additional terms + + a. This License does not allow You to impose (except subject to + Section 4.7 b.) any terms or any technological measures on the + Database, a Derivative Database, or the whole or a Substantial part of + the Contents that alter or restrict the terms of this License, or any + rights granted under it, or have the effect or intent of restricting + the ability of any person to exercise those rights. + + b. Parallel distribution. You may impose terms or technological + measures on the Database, a Derivative Database, or the whole or a + Substantial part of the Contents (a "Restricted Database") in + contravention of Section 4.74 a. only if You also make a copy of the + Database or a Derivative Database available to the recipient of the + Restricted Database: + + i. That is available without additional fee; + + ii. That is available in a medium that does not alter or restrict + the terms of this License, or any rights granted under it, or have + the effect or intent of restricting the ability of any person to + exercise those rights (an "Unrestricted Database"); and + + iii. The Unrestricted Database is at least as accessible to the + recipient as a practical matter as the Restricted Database. + + c. For the avoidance of doubt, You may place this Database or a + Derivative Database in an authenticated environment, behind a + password, or within a similar access control scheme provided that You + do not alter or restrict the terms of this License or any rights + granted under it or have the effect or intent of restricting the + ability of any person to exercise those rights. + +4.8 Licensing of others. You may not sublicense the Database. Each time +You communicate the Database, the whole or Substantial part of the +Contents, or any Derivative Database to anyone else in any way, the +Licensor offers to the recipient a license to the Database on the same +terms and conditions as this License. You are not responsible for +enforcing compliance by third parties with this License, but You may +enforce any rights that You have over a Derivative Database. You are +solely responsible for any modifications of a Derivative Database made +by You or another Person at Your direction. You may not impose any +further restrictions on the exercise of the rights granted or affirmed +under this License. + +### 5.0 Moral rights + +5.1 Moral rights. This section covers moral rights, including any rights +to be identified as the author of the Database or to object to treatment +that would otherwise prejudice the author's honour and reputation, or +any other derogatory treatment: + + a. For jurisdictions allowing waiver of moral rights, Licensor waives + all moral rights that Licensor may have in the Database to the fullest + extent possible by the law of the relevant jurisdiction under Section + 10.4; + + b. If waiver of moral rights under Section 5.1 a in the relevant + jurisdiction is not possible, Licensor agrees not to assert any moral + rights over the Database and waives all claims in moral rights to the + fullest extent possible by the law of the relevant jurisdiction under + Section 10.4; and + + c. For jurisdictions not allowing waiver or an agreement not to assert + moral rights under Section 5.1 a and b, the author may retain their + moral rights over certain aspects of the Database. + +Please note that some jurisdictions do not allow for the waiver of moral +rights, and so moral rights may still subsist over the Database in some +jurisdictions. + +### 6.0 Fair dealing, Database exceptions, and other rights not affected + +6.1 This License does not affect any rights that You or anyone else may +independently have under any applicable law to make any use of this +Database, including without limitation: + + a. Exceptions to the Database Right including: Extraction of Contents + from non-electronic Databases for private purposes, Extraction for + purposes of illustration for teaching or scientific research, and + Extraction or Re-utilisation for public security or an administrative + or judicial procedure. + + b. Fair dealing, fair use, or any other legally recognised limitation + or exception to infringement of copyright or other applicable laws. + +6.2 This License does not affect any rights of lawful users to Extract +and Re-utilise insubstantial parts of the Contents, evaluated +quantitatively or qualitatively, for any purposes whatsoever, including +creating a Derivative Database (subject to other rights over the +Contents, see Section 2.4). The repeated and systematic Extraction or +Re-utilisation of insubstantial parts of the Contents may however amount +to the Extraction or Re-utilisation of a Substantial part of the +Contents. + +### 7.0 Warranties and Disclaimer + +7.1 The Database is licensed by the Licensor "as is" and without any +warranty of any kind, either express, implied, or arising by statute, +custom, course of dealing, or trade usage. Licensor specifically +disclaims any and all implied warranties or conditions of title, +non-infringement, accuracy or completeness, the presence or absence of +errors, fitness for a particular purpose, merchantability, or otherwise. +Some jurisdictions do not allow the exclusion of implied warranties, so +this exclusion may not apply to You. + +### 8.0 Limitation of liability + +8.1 Subject to any liability that may not be excluded or limited by law, +the Licensor is not liable for, and expressly excludes, all liability +for loss or damage however and whenever caused to anyone by any use +under this License, whether by You or by anyone else, and whether caused +by any fault on the part of the Licensor or not. This exclusion of +liability includes, but is not limited to, any special, incidental, +consequential, punitive, or exemplary damages such as loss of revenue, +data, anticipated profits, and lost business. This exclusion applies +even if the Licensor has been advised of the possibility of such +damages. + +8.2 If liability may not be excluded by law, it is limited to actual and +direct financial loss to the extent it is caused by proved negligence on +the part of the Licensor. + +### 9.0 Termination of Your rights under this License + +9.1 Any breach by You of the terms and conditions of this License +automatically terminates this License with immediate effect and without +notice to You. For the avoidance of doubt, Persons who have received the +Database, the whole or a Substantial part of the Contents, Derivative +Databases, or the Database as part of a Collective Database from You +under this License will not have their licenses terminated provided +their use is in full compliance with this License or a license granted +under Section 4.8 of this License. Sections 1, 2, 7, 8, 9 and 10 will +survive any termination of this License. + +9.2 If You are not in breach of the terms of this License, the Licensor +will not terminate Your rights under it. + +9.3 Unless terminated under Section 9.1, this License is granted to You +for the duration of applicable rights in the Database. + +9.4 Reinstatement of rights. If you cease any breach of the terms and +conditions of this License, then your full rights under this License +will be reinstated: + + a. Provisionally and subject to permanent termination until the 60th + day after cessation of breach; + + b. Permanently on the 60th day after cessation of breach unless + otherwise reasonably notified by the Licensor; or + + c. Permanently if reasonably notified by the Licensor of the + violation, this is the first time You have received notice of + violation of this License from the Licensor, and You cure the + violation prior to 30 days after your receipt of the notice. + +Persons subject to permanent termination of rights are not eligible to +be a recipient and receive a license under Section 4.8. + +9.5 Notwithstanding the above, Licensor reserves the right to release +the Database under different license terms or to stop distributing or +making available the Database. Releasing the Database under different +license terms or stopping the distribution of the Database will not +withdraw this License (or any other license that has been, or is +required to be, granted under the terms of this License), and this +License will continue in full force and effect unless terminated as +stated above. + +### 10.0 General + +10.1 If any provision of this License is held to be invalid or +unenforceable, that must not affect the validity or enforceability of +the remainder of the terms and conditions of this License and each +remaining provision of this License shall be valid and enforced to the +fullest extent permitted by law. + +10.2 This License is the entire agreement between the parties with +respect to the rights granted here over the Database. It replaces any +earlier understandings, agreements or representations with respect to +the Database. + +10.3 If You are in breach of the terms of this License, You will not be +entitled to rely on the terms of this License or to complain of any +breach by the Licensor. + +10.4 Choice of law. This License takes effect in and will be governed by +the laws of the relevant jurisdiction in which the License terms are +sought to be enforced. If the standard suite of rights granted under +applicable copyright law and Database Rights in the relevant +jurisdiction includes additional rights not granted under this License, +these additional rights are granted in this License in order to meet the +terms of this License. diff --git a/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/NOTICE.txt b/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/NOTICE.txt new file mode 100644 index 0000000000000..33be0fd0e724b --- /dev/null +++ b/x-pack/plugin/spatial/src/test/resources/org/elasticsearch/xpack/spatial/index/fielddata/NOTICE.txt @@ -0,0 +1,2 @@ +The Antarctica.wkt.gz and France.wkt.gz files come from the OpenStreetMap project (© OpenStreetMap contributors), +which is made available here under the Open Database License (ODbL). diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/70_script_doc_values.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/70_script_doc_values.yml index 422afd9522aef..aa2b3812c1c87 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/70_script_doc_values.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/spatial/70_script_doc_values.yml @@ -47,6 +47,19 @@ setup: - match: { hits.hits.0.fields.bbox.0.bottom_right.lat: 59.94124996941537 } - match: { hits.hits.0.fields.bbox.0.bottom_right.lon: 24.048749981448054 } +--- +"label position": + - do: + search: + rest_total_hits_as_int: true + body: + script_fields: + label_position: + script: + source: "doc['geo_shape'].getLabelPosition()" + - match: { hits.hits.0.fields.label_position.0.lat: 59.942043484188616 } + - match: { hits.hits.0.fields.label_position.0.lon: 24.047588920220733 } + --- "bounding box points": - do: