Skip to content

Commit 6dddfdc

Browse files
author
Bapt Abl
committed
Merge branch '7.17'
2 parents 1c87591 + 8ae49d0 commit 6dddfdc

File tree

5 files changed

+587
-47
lines changed

5 files changed

+587
-47
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### 7.17.1.1
2+
3+
* Fix deduplication of points
4+
* Fix handling of GeometryCollections
5+
* Add tests

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
elastic_version = 7.17.1
2-
plugin_version = 7.17.1.0
2+
plugin_version = 7.17.1.1

src/main/java/org/opendatasoft/elasticsearch/ingest/GeoExtensionProcessor.java

Lines changed: 116 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
import org.locationtech.jts.geom.Coordinate;
2020
import org.locationtech.jts.geom.Geometry;
2121
import org.locationtech.jts.geom.GeometryFactory;
22-
import org.locationtech.jts.geom.Point;
2322
import org.locationtech.jts.geom.PrecisionModel;
2423
import org.locationtech.jts.io.ParseException;
2524
import org.locationtech.jts.io.WKBWriter;
25+
import org.locationtech.jts.io.WKTReader;
2626
import org.locationtech.jts.io.WKTWriter;
27+
import org.locationtech.spatial4j.exception.InvalidShapeException;
2728
import org.locationtech.spatial4j.shape.Shape;
2829
import org.locationtech.spatial4j.shape.jts.JtsGeometry;
2930
import org.locationtech.spatial4j.shape.jts.JtsPoint;
@@ -50,6 +51,10 @@ public class GeoExtensionProcessor extends AbstractProcessor {
5051
private final String bboxField;
5152
private final String centroidField;
5253

54+
private final GeometryFactory geomFactory;
55+
private final WKTWriter wktWriter;
56+
private final WKTReader wktReader;
57+
5358
private GeoExtensionProcessor(String tag, String description, String field, String path, Boolean keepShape, String shapeField,
5459
String fixedField, String wkbField, String hashField, String typeField,
5560
String areaField, String bboxField, String centroidField) {
@@ -65,6 +70,12 @@ private GeoExtensionProcessor(String tag, String description, String field, Stri
6570
this.areaField = areaField;
6671
this.bboxField = bboxField;
6772
this.centroidField = centroidField;
73+
74+
PrecisionModel precisionModel = new PrecisionModel(PrecisionModel.FLOATING);
75+
this.geomFactory = new GeometryFactory(precisionModel, 0);
76+
77+
this.wktWriter = new WKTWriter();
78+
this.wktReader = new WKTReader();
6879
}
6980

7081
@SuppressWarnings("unchecked")
@@ -102,6 +113,72 @@ private List<String> getGeoShapeFieldsFromDoc(IngestDocument ingestDocument) {
102113
return ShapeParser.parse(parser);
103114
}
104115

116+
private class GeometryWithWkt {
117+
public Geometry geometry;
118+
public String wkt;
119+
}
120+
121+
private GeometryWithWkt s4jToJts(Shape shape) {
122+
// Convert a Spatial4J shape into a JTSGeometry
123+
// A Spatial4J shape is basically either:
124+
// - a JtsPoint
125+
// - a JtsGeometry
126+
// - an XShapeCollection (multi-* and GeometryCollection)
127+
// There is a special case for MultiPoint since the WKTWriter of JTS
128+
// is not compliant with the wkt reader of ES.
129+
130+
GeometryWithWkt geomWithWkt = new GeometryWithWkt();
131+
Geometry geom = null;
132+
133+
String altWKT = null;
134+
if (shape instanceof JtsPoint) {
135+
geom = ((JtsPoint) shape).getGeom();
136+
} else if (shape instanceof JtsGeometry) {
137+
geom = ((JtsGeometry) shape).getGeom();
138+
} else if (shape instanceof XShapeCollection) {
139+
XShapeCollection collection = (XShapeCollection) shape;
140+
ArrayList<Geometry> geoms = new ArrayList<>(collection.size());
141+
ArrayList<String> wkts = new ArrayList<>(collection.size());
142+
143+
boolean hasWkt = false;
144+
for (int i = 0; i < collection.size(); i++) {
145+
GeometryWithWkt g = s4jToJts(collection.get(i));
146+
geoms.add(g.geometry);
147+
wkts.add(g.wkt);
148+
if (g.wkt != null) {
149+
hasWkt = true;
150+
}
151+
}
152+
153+
if (hasWkt) {
154+
// if it has a WKT, it means it is a geometrycolleciton
155+
// containing a multipoint
156+
altWKT = "GEOMETRYCOLLECTION(";
157+
for (int i = 0; i < collection.size(); i++) {
158+
if (wkts.get(i) != null) {
159+
altWKT += wkts.get(i);
160+
} else {
161+
altWKT += wktWriter.write(geoms.get(i));
162+
}
163+
if (i < collection.size() - 1) {
164+
altWKT += ",";
165+
}
166+
}
167+
altWKT += ")";
168+
}
169+
geom = geomFactory.buildGeometry(geoms);
170+
171+
if (geom.getGeometryType() == "MultiPoint") {
172+
// special case of multipoint where the WKT must be corrected
173+
// ES wants multipoint without extra parenthesis between points
174+
altWKT = wktWriter.write(geom).replace("((", "(").replace("))", ")").replace("), (", ", ");
175+
}
176+
}
177+
geomWithWkt.geometry = geom;
178+
geomWithWkt.wkt = altWKT;
179+
return geomWithWkt;
180+
}
181+
105182
@SuppressWarnings("unchecked")
106183
@Override
107184
public IngestDocument execute(IngestDocument ingestDocument) throws IOException, ParseException {
@@ -114,47 +191,50 @@ public IngestDocument execute(IngestDocument ingestDocument) throws IOException,
114191
continue;
115192
}
116193

194+
// From an input Object that represents a GeoJSON we need to:
195+
// - store a WKT version of the geometry, that will also be indexed as a geometry by ES
196+
// - make sure this WKT does not have duplicated points, since ES refuse them
197+
// - make sure this WKT geometry is equal (or close enough) to the geometry indexed by ES (why ?).
198+
// It means if ES splits the geometry around the dateline, we want to do the same
199+
// - compute its area
200+
// - compute its WKB representation
201+
// - compute its centroid
202+
//
203+
// We currently do this by:
204+
// 1/ creating a Spatial4J shape from an Object (thanks to legacygeo.ShapeBuilder)
205+
// 2/ converting this Spatial4J shape to a JTSGeometry
206+
// 3/ call area(), wkbwriter(), centroid() and wktwriter on the JTSGeometry
207+
//
208+
// 2/ could be avoided but the S4J WKTWriter is of poor quality (e.g. a multipoint can be represented as a geometrycollection of points)
209+
// 1/ could be avoided if we find how to create a JTSGeometry from an Object
210+
117211
ShapeBuilder<?,?, ?> shapeBuilder = getShapeBuilderFromObject(geoShapeObject);
118212

119213
// buildS4J() will try to clean up and fix the shape. If it fails, an exception is raised
120214
// Included fixes:
121-
// - point deduplication
122215
// - dateline warping (enforce lon in [-180,180])
123-
Shape shape = shapeBuilder.buildS4J();
124-
125-
Geometry geom = null;
126-
PrecisionModel precisionModel = new PrecisionModel(PrecisionModel.FLOATING);
127-
GeometryFactory geomFactory = new GeometryFactory(precisionModel, 0);
128-
String altWKT = null;
129-
if (shape instanceof JtsPoint) {
130-
geom = ((JtsPoint)shape).getGeom();
131-
}
132-
else if (shape instanceof JtsGeometry) {
133-
geom = ((JtsGeometry) shape).getGeom();
134-
}
135-
else if (shape instanceof XShapeCollection) {
136-
List<?> shapes = ((XShapeCollection) shape).getShapes();
137-
switch (shapeBuilder.type()) {
138-
case MULTIPOINT:
139-
Point[] points = new Point[shapes.size()];
140-
for (int i = 0; i < shapes.size(); i++) {
141-
points[i] = (Point)((JtsPoint)(shapes.get(i))).getGeom();
142-
}
143-
geom = geomFactory.createMultiPoint(points);
144-
// ES wants multipoint without extra parenthesis between points
145-
altWKT = new WKTWriter().write(geom).replace("((","(").replace("))",")").replace("), (",", ");
146-
break;
147-
case MULTILINESTRING:
148-
case MULTIPOLYGON:
149-
case GEOMETRYCOLLECTION:
150-
ArrayList<Geometry> geoms = new ArrayList<>(shapes.size());
151-
for (int i = 0; i < shapes.size(); i++) {
152-
geoms.add(((JtsGeometry)(shapes.get(i))).getGeom());
153-
}
154-
geom = geomFactory.buildGeometry(geoms);
155-
break;
216+
Shape shape = null;
217+
try {
218+
try {
219+
shape = shapeBuilder.buildS4J();
220+
} catch (InvalidShapeException e) {
221+
// buildS4J does not always deduplicate points
222+
shapeBuilder = GeoUtils.removeDuplicateCoordinates(shapeBuilder);
223+
shape = shapeBuilder.buildS4J();
156224
}
157225
}
226+
catch (Throwable e) {
227+
// sometimes it is still not enough
228+
// e.g. when non-EPSG:4326 coordinates are input
229+
// buildS4J will try to warp date lines and may generate lots of small components
230+
// which could yield a GeometryCollection, which raises an AssertionError
231+
// So, we catch here both Error (AssertionError) and Exceptions
232+
throw new IllegalArgumentException("Unable to parse shape [" + shapeBuilder.toWKT() + "]: " + e.getMessage());
233+
}
234+
235+
GeometryWithWkt geometryWithWkt = s4jToJts(shape);
236+
Geometry geom = geometryWithWkt.geometry;
237+
String altWKT = geometryWithWkt.wkt;
158238

159239
if (geom == null) {
160240
throw new IllegalArgumentException("Unable to parse shape [" + shapeBuilder.toWKT() + "]");
@@ -168,7 +248,7 @@ else if (shape instanceof XShapeCollection) {
168248

169249
if (fixedField != null) {
170250
ingestDocument.setFieldValue(geoShapeField + "." + fixedField,
171-
altWKT != null ? altWKT : new WKTWriter().write(geom));
251+
altWKT != null ? altWKT : wktWriter.write(geom));
172252
}
173253

174254
// compute and add extra geo sub-fields

src/main/java/org/opendatasoft/elasticsearch/plugin/GeoUtils.java

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
import org.apache.lucene.util.BytesRef;
44
import org.elasticsearch.common.geo.GeoPoint;
5+
import org.elasticsearch.common.geo.Orientation;
56
import org.elasticsearch.common.hash.MurmurHash3;
7+
import org.elasticsearch.geometry.Point;
8+
import org.elasticsearch.legacygeo.builders.GeometryCollectionBuilder;
9+
import org.elasticsearch.legacygeo.builders.LineStringBuilder;
10+
import org.elasticsearch.legacygeo.builders.MultiPolygonBuilder;
11+
import org.elasticsearch.legacygeo.builders.PolygonBuilder;
12+
import org.elasticsearch.legacygeo.builders.ShapeBuilder;
613
import org.locationtech.jts.geom.Coordinate;
714
import org.locationtech.jts.geom.Geometry;
815
import org.locationtech.jts.geom.GeometryCollection;
@@ -13,9 +20,16 @@
1320
import org.locationtech.jts.io.WKBWriter;
1421
import org.locationtech.jts.io.WKTWriter;
1522
import org.locationtech.jts.io.geojson.GeoJsonWriter;
23+
import org.elasticsearch.geometry.Line;
1624

25+
import java.util.ArrayList;
1726
import java.util.Arrays;
27+
import java.util.Collection;
28+
import java.util.HashMap;
29+
import java.util.Iterator;
1830
import java.util.List;
31+
import java.util.ListIterator;
32+
import java.util.Vector;
1933

2034
public class GeoUtils {
2135
public enum OutputFormat {
@@ -112,23 +126,62 @@ public static String exportGeoTo(Geometry geom, OutputFormat outputFormat, GeoJs
112126
}
113127
}
114128

115-
public static Geometry removeDuplicateCoordinates(Geometry geom) {
116-
if (geom.isEmpty()) {
117-
return geom;
129+
public static LineStringBuilder removeDuplicateCoordinates(LineStringBuilder builder) {
130+
Line line = (Line)builder.buildGeometry(); // no direct access to coordinates
131+
Vector<Coordinate> newCoordinates = new Vector<Coordinate>();
132+
133+
Point previous = null;
134+
for (int i = 0; i < line.length(); i++) {
135+
Point current = new Point(line.getX(i), line.getY(i));
136+
if ((previous != null) && (previous.equals(current))) {
137+
continue;
138+
}
139+
newCoordinates.add(new Coordinate(current.getX(), current.getY()));
140+
previous = current;
118141
}
142+
return new LineStringBuilder(newCoordinates);
143+
}
119144

120-
if (geom instanceof Polygon) {
121-
return removeDuplicateCoordinates((Polygon) geom);
145+
public static PolygonBuilder removeDuplicateCoordinates(PolygonBuilder builder) {
146+
LineStringBuilder exteriorBuilder = removeDuplicateCoordinates(builder.shell());
147+
PolygonBuilder pb = new PolygonBuilder(exteriorBuilder, /* unused */Orientation.RIGHT, true);
148+
List<LineStringBuilder> holes = builder.holes();
149+
for (int i = 0; i < holes.size(); i++) {
150+
pb.hole(removeDuplicateCoordinates(holes.get(i)), true);
122151
}
152+
return pb;
153+
}
123154

124-
if (geom instanceof MultiPolygon) {
125-
return removeDuplicateCoordinates((MultiPolygon) geom);
155+
public static MultiPolygonBuilder removeDuplicateCoordinates(MultiPolygonBuilder builder) {
156+
MultiPolygonBuilder mpb = new MultiPolygonBuilder();
157+
List<PolygonBuilder> polygons = builder.polygons();
158+
for (int i = 0; i < polygons.size(); i++) {
159+
mpb.polygon(removeDuplicateCoordinates(polygons.get(i)));
126160
}
161+
return mpb;
162+
}
127163

128-
if (geom instanceof GeometryCollection) {
129-
return removeDuplicateCoordinates((GeometryCollection) geom);
164+
public static GeometryCollectionBuilder removeDuplicateCoordinates(GeometryCollectionBuilder builder) {
165+
GeometryCollectionBuilder gcb = new GeometryCollectionBuilder();
166+
for (int i = 0; i < builder.numShapes(); i++) {
167+
gcb.shape(removeDuplicateCoordinates(builder.getShapeAt(i)));
130168
}
169+
return gcb;
170+
}
131171

132-
return geom;
172+
public static ShapeBuilder removeDuplicateCoordinates(ShapeBuilder builder){
173+
if (builder instanceof LineStringBuilder) {
174+
return removeDuplicateCoordinates((LineStringBuilder)builder);
175+
}
176+
if (builder instanceof PolygonBuilder) {
177+
return removeDuplicateCoordinates((PolygonBuilder)builder);
178+
}
179+
if (builder instanceof MultiPolygonBuilder) {
180+
return removeDuplicateCoordinates((MultiPolygonBuilder)builder);
181+
}
182+
if (builder instanceof GeometryCollectionBuilder) {
183+
return removeDuplicateCoordinates((GeometryCollectionBuilder) builder);
184+
}
185+
return builder;
133186
}
134187
}

0 commit comments

Comments
 (0)