Skip to content

Commit 00aa197

Browse files
authored
Fields API should return normalize geometries (#80649)
Fields API deliver now the normalize geometries instead of the plain geometries from source Co-authored-by: James Rodewig <[email protected]
1 parent 3a3bb23 commit 00aa197

File tree

17 files changed

+423
-16
lines changed

17 files changed

+423
-16
lines changed

docs/reference/migration/migrate_8_1.asciidoc

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@ See also <<release-highlights>> and <<es-release-notes>>.
1111

1212
coming[8.1.0]
1313

14-
////
1514
[discrete]
1615
[[breaking-changes-8.1]]
1716
=== Breaking changes
1817

1918
The following changes in {es} 8.1 might affect your applications
2019
and prevent them from operating normally.
21-
Before upgrading to 8.0, review these changes and take the described steps
20+
Before upgrading to 8.1, review these changes and take the described steps
2221
to mitigate the impact.
2322

2423
NOTE: Breaking changes introduced in minor versions are
@@ -32,9 +31,25 @@ enable <<deprecation-logging, deprecation logging>>.
3231
//Installation and Upgrade Guide
3332

3433
//tag::notable-breaking-changes[]
34+
[discrete]
35+
[[breaking_81_rest_api_changes]]
36+
==== REST API changes
37+
38+
[[fields_api_geoshape_normalize]]
39+
.The search API's `fields` parameter now normalizes geometry objects that cross the international dateline.
40+
[%collapsible]
41+
====
42+
*Details* +
43+
The search API's `fields` parameter now normalizes `geo_shape` objects that
44+
cross the international dateline (+/-180° longitude). For example, if a polygon
45+
crosses the dateline, the `fields` parameter returns it as two polygons. You can
46+
still retrieve original, unnormalized geometry objects from `_source`.
3547
48+
*Impact* +
49+
If your application requires unnormalized geometry objects, retrieve them from
50+
`_source` rather than using the `fields` parameter.
51+
====
3652
//end::notable-breaking-changes[]
37-
////
3853

3954
[discrete]
4055
[[deprecated-8.1]]
@@ -52,7 +67,6 @@ To find out if you are using any deprecated functionality,
5267
enable <<deprecation-logging, deprecation logging>>.
5368

5469
//tag::notable-breaking-changes[]
55-
5670
[discrete]
5771
[[breaking_8.1_cluster_node_setting_deprecations]]
5872
==== Cluster and node setting deprecations

modules/legacy-geo/src/main/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,11 @@ public void parse(
403403
onMalformed.accept(e);
404404
}
405405
}
406+
407+
@Override
408+
public ShapeBuilder<?, ?, ?> normalizeFromSource(ShapeBuilder<?, ?, ?> geometry) {
409+
return geometry;
410+
}
406411
}
407412

408413
public static final class GeoShapeFieldType extends AbstractShapeGeometryFieldType<ShapeBuilder<?, ?, ?>> implements GeoShapeQueryable {

server/src/main/java/org/elasticsearch/common/geo/GeoLineDecomposer.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ private GeoLineDecomposer() {
2727
// no instances
2828
}
2929

30+
/**
31+
* Checks if the provided line needs to be split by the dateline.
32+
*/
33+
public static boolean needsDecomposing(Line line) {
34+
for (int i = 0; i < line.length(); i++) {
35+
if (GeoUtils.needsNormalizeLat(line.getLat(i)) || GeoUtils.needsNormalizeLon(line.getLon(i))) {
36+
return true;
37+
}
38+
}
39+
return false;
40+
}
41+
42+
/**
43+
* Splits the specified lines in the Multiline by datelines and adds them to the supplied lines array
44+
*/
3045
public static void decomposeMultiLine(MultiLine multiLine, List<Line> collector) {
3146
for (Line line : multiLine) {
3247
decomposeLine(line, collector);

server/src/main/java/org/elasticsearch/common/geo/GeoPolygonDecomposer.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ private GeoPolygonDecomposer() {
3939
// no instances
4040
}
4141

42+
public static boolean needsDecomposing(Polygon polygon) {
43+
double minX = Double.POSITIVE_INFINITY;
44+
double maxX = Double.NEGATIVE_INFINITY;
45+
LinearRing linearRing = polygon.getPolygon();
46+
for (int i = 0; i < linearRing.length(); i++) {
47+
if (GeoUtils.needsNormalizeLat(linearRing.getLat(i)) || GeoUtils.needsNormalizeLon(linearRing.getLon(i))) {
48+
return true;
49+
}
50+
minX = Math.min(minX, linearRing.getLon(i));
51+
maxX = Math.max(maxX, linearRing.getLon(i));
52+
}
53+
// calculate range
54+
final double rng = maxX - minX;
55+
// we need to decompose tif the range is greater than a hemisphere (180 degrees)
56+
// but not spanning 2 hemispheres (translation would result in a collapsed poly)
57+
return rng > DATELINE && rng != 2 * DATELINE;
58+
}
59+
4260
public static void decomposeMultiPolygon(MultiPolygon multiPolygon, boolean orientation, List<Polygon> collector) {
4361
for (Polygon polygon : multiPolygon) {
4462
decomposePolygon(polygon, orientation, collector);

server/src/main/java/org/elasticsearch/common/geo/GeoUtils.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,19 @@ public static void normalizePoint(double[] lonLat) {
293293
normalizePoint(lonLat, true, true);
294294
}
295295

296+
public static boolean needsNormalizeLat(double lat) {
297+
return lat > 90 || lat < -90;
298+
}
299+
300+
public static boolean needsNormalizeLon(double lon) {
301+
return lon > 180 || lon < -180;
302+
}
303+
296304
public static void normalizePoint(double[] lonLat, boolean normLon, boolean normLat) {
297305
assert lonLat != null && lonLat.length == 2;
298306

299-
normLat = normLat && (lonLat[1] > 90 || lonLat[1] < -90);
300-
normLon = normLon && (lonLat[0] > 180 || lonLat[0] < -180 || normLat);
307+
normLat = normLat && needsNormalizeLat(lonLat[1]);
308+
normLon = normLon && (needsNormalizeLon(lonLat[0]) || normLat);
301309

302310
if (normLat) {
303311
lonLat[1] = centeredModulus(lonLat[1], 360);

server/src/main/java/org/elasticsearch/common/geo/GeometryNormalizer.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.ArrayList;
2525
import java.util.List;
2626

27+
import static org.elasticsearch.common.geo.GeoUtils.needsNormalizeLat;
28+
import static org.elasticsearch.common.geo.GeoUtils.needsNormalizeLon;
2729
import static org.elasticsearch.common.geo.GeoUtils.normalizePoint;
2830

2931
/**
@@ -160,4 +162,90 @@ public Geometry visit(Rectangle rectangle) {
160162
}
161163
});
162164
}
165+
166+
/**
167+
* Return false if the provided {@link Geometry} is already Lucene friendly,
168+
* else return false.
169+
*/
170+
public static boolean needsNormalize(Orientation orientation, Geometry geometry) {
171+
if (geometry == null) {
172+
return false;
173+
}
174+
175+
return geometry.visit(new GeometryVisitor<>() {
176+
@Override
177+
public Boolean visit(Circle circle) {
178+
if (circle.isEmpty()) {
179+
return Boolean.FALSE;
180+
}
181+
return needsNormalizeLat(circle.getLat()) || needsNormalizeLon(circle.getLon());
182+
}
183+
184+
@Override
185+
public Boolean visit(GeometryCollection<?> collection) {
186+
for (Geometry shape : collection) {
187+
if (shape.visit(this)) {
188+
return Boolean.TRUE;
189+
}
190+
}
191+
return Boolean.FALSE;
192+
}
193+
194+
@Override
195+
public Boolean visit(Line line) {
196+
return GeoLineDecomposer.needsDecomposing(line);
197+
}
198+
199+
@Override
200+
public Boolean visit(LinearRing ring) {
201+
throw new IllegalArgumentException("invalid shape type found [LinearRing]");
202+
}
203+
204+
@Override
205+
public Boolean visit(MultiLine multiLine) {
206+
for (Line line : multiLine) {
207+
if (visit(line)) {
208+
return Boolean.TRUE;
209+
}
210+
}
211+
return Boolean.FALSE;
212+
}
213+
214+
@Override
215+
public Boolean visit(MultiPoint multiPoint) {
216+
for (Point point : multiPoint) {
217+
if (visit(point)) {
218+
return Boolean.TRUE;
219+
}
220+
}
221+
return Boolean.FALSE;
222+
}
223+
224+
@Override
225+
public Boolean visit(MultiPolygon multiPolygon) {
226+
for (Polygon polygon : multiPolygon) {
227+
if (visit(polygon)) {
228+
return Boolean.TRUE;
229+
}
230+
}
231+
return Boolean.FALSE;
232+
}
233+
234+
@Override
235+
public Boolean visit(Point point) {
236+
return needsNormalizeLat(point.getLat()) || needsNormalizeLon(point.getLon());
237+
}
238+
239+
@Override
240+
public Boolean visit(Polygon polygon) {
241+
return GeoPolygonDecomposer.needsDecomposing(polygon);
242+
}
243+
244+
@Override
245+
public Boolean visit(Rectangle rectangle) {
246+
// TODO: what happen with rectangles over the dateline
247+
return Boolean.FALSE;
248+
}
249+
});
250+
}
163251
}

server/src/main/java/org/elasticsearch/index/mapper/AbstractGeometryFieldMapper.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,19 @@ public abstract void parse(XContentParser parser, CheckedConsumer<T, IOException
5454

5555
private void fetchFromSource(Object sourceMap, Consumer<T> consumer) {
5656
try (XContentParser parser = MapXContentParser.wrapObject(sourceMap)) {
57-
parse(parser, v -> consumer.accept(v), e -> {}); /* ignore malformed */
57+
parse(parser, v -> consumer.accept(normalizeFromSource(v)), e -> {}); /* ignore malformed */
5858
} catch (IOException e) {
5959
throw new UncheckedIOException(e);
6060
}
6161
}
6262

63+
/**
64+
* Normalize a geometry when reading from source. When reading from source we can skip
65+
* some expensive steps as the geometry has already been indexed.
66+
*/
67+
// TODO: move geometry normalization to the geometry parser.
68+
public abstract T normalizeFromSource(T geometry);
69+
6370
}
6471

6572
public abstract static class AbstractGeometryFieldType<T> extends MappedFieldType {

server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,5 +368,11 @@ private boolean isNormalizable(double coord) {
368368
protected void reset(GeoPoint in, double x, double y) {
369369
in.reset(y, x);
370370
}
371+
372+
@Override
373+
public GeoPoint normalizeFromSource(GeoPoint point) {
374+
// normalize during parsing
375+
return point;
376+
}
371377
}
372378
}

server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public GeoShapeFieldMapper build(MapperBuilderContext context) {
101101
coerce.get().value(),
102102
ignoreZValue.get().value()
103103
);
104-
GeoShapeParser geoShapeParser = new GeoShapeParser(geometryParser);
104+
GeoShapeParser geoShapeParser = new GeoShapeParser(geometryParser, orientation.get().value());
105105
GeoShapeFieldType ft = new GeoShapeFieldType(
106106
context.buildFullName(name),
107107
indexed.get(),

server/src/main/java/org/elasticsearch/index/mapper/GeoShapeParser.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
package org.elasticsearch.index.mapper;
1010

1111
import org.elasticsearch.ElasticsearchParseException;
12+
import org.elasticsearch.common.geo.GeometryNormalizer;
1213
import org.elasticsearch.common.geo.GeometryParser;
14+
import org.elasticsearch.common.geo.Orientation;
1315
import org.elasticsearch.core.CheckedConsumer;
1416
import org.elasticsearch.geometry.Geometry;
1517
import org.elasticsearch.xcontent.XContentParser;
@@ -20,9 +22,11 @@
2022

2123
public class GeoShapeParser extends AbstractGeometryFieldMapper.Parser<Geometry> {
2224
private final GeometryParser geometryParser;
25+
private final Orientation orientation;
2326

24-
public GeoShapeParser(GeometryParser geometryParser) {
27+
public GeoShapeParser(GeometryParser geometryParser, Orientation orientation) {
2528
this.geometryParser = geometryParser;
29+
this.orientation = orientation;
2630
}
2731

2832
@Override
@@ -40,4 +44,17 @@ public void parse(XContentParser parser, CheckedConsumer<Geometry, IOException>
4044
onMalformed.accept(e);
4145
}
4246
}
47+
48+
@Override
49+
public Geometry normalizeFromSource(Geometry geometry) {
50+
// GeometryNormalizer contains logic for validating the input geometry,
51+
// so it needs to be run always at indexing time. When run over source we can skip
52+
// the validation, and we run normalization (which is expensive) only when we need
53+
// to split geometries around the dateline.
54+
if (GeometryNormalizer.needsNormalize(orientation, geometry)) {
55+
return GeometryNormalizer.apply(orientation, geometry);
56+
} else {
57+
return geometry;
58+
}
59+
}
4360
}

0 commit comments

Comments
 (0)