Skip to content

Commit e1c060a

Browse files
authored
Add Circle Processor (#43851)
add circle-processor that translates circles to polygons
1 parent 8d16c9b commit e1c060a

File tree

10 files changed

+898
-1
lines changed

10 files changed

+898
-1
lines changed
193 KB
Loading

docs/reference/ingest/ingest-node.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,7 @@ See {plugins}/ingest.html[Ingest plugins] for information about the available in
839839

840840
include::processors/append.asciidoc[]
841841
include::processors/bytes.asciidoc[]
842+
include::processors/circle.asciidoc[]
842843
include::processors/convert.asciidoc[]
843844
include::processors/date.asciidoc[]
844845
include::processors/date-index-name.asciidoc[]
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
[role="xpack"]
2+
[testenv="basic"]
3+
[[ingest-circle-processor]]
4+
=== Circle Processor
5+
Converts circle definitions of shapes to regular polygons which approximate them.
6+
7+
[[circle-processor-options]]
8+
.Circle Processor Options
9+
[options="header"]
10+
|======
11+
| Name | Required | Default | Description
12+
| `field` | yes | - | The string-valued field to trim whitespace from
13+
| `target_field` | no | `field` | The field to assign the polygon shape to, by default `field` is updated in-place
14+
| `ignore_missing` | no | `false` | If `true` and `field` does not exist, the processor quietly exits without modifying the document
15+
| `error_distance` | yes | - | The difference between the resulting inscribed distance from center to side and the circle's radius (measured in meters for `geo_shape`, unit-less for `shape`)
16+
| `shape_type` | yes | - | which field mapping type is to be used when processing the circle: `geo_shape` or `shape`
17+
include::common-options.asciidoc[]
18+
|======
19+
20+
21+
image:images/spatial/error_distance.png[]
22+
23+
[source,js]
24+
--------------------------------------------------
25+
PUT circles
26+
{
27+
"mappings": {
28+
"properties": {
29+
"circle": {
30+
"type": "geo_shape"
31+
}
32+
}
33+
}
34+
}
35+
36+
PUT _ingest/pipeline/polygonize_circles
37+
{
38+
"description": "translate circle to polygon",
39+
"processors": [
40+
{
41+
"circle": {
42+
"field": "circle",
43+
"error_distance": 28.0,
44+
"shape_type": "geo_shape"
45+
}
46+
}
47+
]
48+
}
49+
--------------------------------------------------
50+
// CONSOLE
51+
52+
Using the above pipeline, we can attempt to index a document into the `circles` index.
53+
The circle can be represented as either a WKT circle or a GeoJSON circle. The resulting
54+
polygon will be represented and indexed using the same format as the input circle. WKT will
55+
be translated to a WKT polygon, and GeoJSON circles will be translated to GeoJSON polygons.
56+
57+
==== Example: Circle defined in Well Known Text
58+
59+
In this example a circle defined in WKT format is indexed
60+
61+
[source,js]
62+
--------------------------------------------------
63+
PUT circles/_doc/1?pipeline=polygonize_circles
64+
{
65+
"circle": "CIRCLE (30 10 40)"
66+
}
67+
68+
GET circles/_doc/1
69+
--------------------------------------------------
70+
// CONSOLE
71+
// TEST[continued]
72+
73+
The response from the above index request:
74+
75+
[source,js]
76+
--------------------------------------------------
77+
{
78+
"found": true,
79+
"_index": "circles",
80+
"_type": "_doc",
81+
"_id": "1",
82+
"_version": 1,
83+
"_seq_no": 22,
84+
"_primary_term": 1,
85+
"_source": {
86+
"circle": "polygon ((30.000365257263184 10.0, 30.000111397193788 10.00034284530941, 29.999706043744222 10.000213571721195, 29.999706043744222 9.999786428278805, 30.000111397193788 9.99965715469059, 30.000365257263184 10.0))"
87+
}
88+
}
89+
--------------------------------------------------
90+
// TESTRESPONSE[s/"_seq_no": \d+/"_seq_no" : $body._seq_no/ s/"_primary_term": 1/"_primary_term" : $body._primary_term/]
91+
92+
==== Example: Circle defined in GeoJSON
93+
94+
In this example a circle defined in GeoJSON format is indexed
95+
96+
[source,js]
97+
--------------------------------------------------
98+
PUT circles/_doc/2?pipeline=polygonize_circles
99+
{
100+
"circle": {
101+
"type": "circle",
102+
"radius": "40m",
103+
"coordinates": [30, 10]
104+
}
105+
}
106+
107+
GET circles/_doc/2
108+
--------------------------------------------------
109+
// CONSOLE
110+
// TEST[continued]
111+
112+
The response from the above index request:
113+
114+
[source,js]
115+
--------------------------------------------------
116+
{
117+
"found": true,
118+
"_index": "circles",
119+
"_type": "_doc",
120+
"_id": "2",
121+
"_version": 1,
122+
"_seq_no": 22,
123+
"_primary_term": 1,
124+
"_source": {
125+
"circle": {
126+
"coordinates": [
127+
[
128+
[30.000365257263184, 10.0],
129+
[30.000111397193788, 10.00034284530941],
130+
[29.999706043744222, 10.000213571721195],
131+
[29.999706043744222, 9.999786428278805],
132+
[30.000111397193788, 9.99965715469059],
133+
[30.000365257263184, 10.0]
134+
]
135+
],
136+
"type": "polygon"
137+
}
138+
}
139+
}
140+
--------------------------------------------------
141+
// TESTRESPONSE[s/"_seq_no": \d+/"_seq_no" : $body._seq_no/ s/"_primary_term": 1/"_primary_term" : $body._primary_term/]
142+
143+
144+
==== Notes on Accuracy
145+
146+
Accuracy of the polygon that represents the circle is defined as `error_distance`. The smaller this
147+
difference is, the closer to a perfect circle the polygon is.
148+
149+
Below is a table that aims to help capture how the radius of the circle affects the resulting number of sides
150+
of the polygon given different inputs.
151+
152+
The minimum number of sides is `4` and the maximum is `1000`.
153+
154+
[[circle-processor-accuracy]]
155+
.Circle Processor Accuracy
156+
[options="header"]
157+
|======
158+
| error_distance | radius in meters | number of sides of polygon
159+
| 1.00 | 1.0 | 4
160+
| 1.00 | 10.0 | 14
161+
| 1.00 | 100.0 | 45
162+
| 1.00 | 1000.0 | 141
163+
| 1.00 | 10000.0 | 445
164+
| 1.00 | 100000.0 | 1000
165+
|======

server/src/main/java/org/elasticsearch/ingest/ConfigurationUtils.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,26 @@ public static Integer readIntProperty(String processorType, String processorTag,
189189
}
190190
}
191191

192+
/**
193+
* Returns and removes the specified property from the specified configuration map.
194+
*
195+
* If the property value isn't of type int a {@link ElasticsearchParseException} is thrown.
196+
* If the property is missing an {@link ElasticsearchParseException} is thrown
197+
*/
198+
public static Double readDoubleProperty(String processorType, String processorTag, Map<String, Object> configuration,
199+
String propertyName) {
200+
Object value = configuration.remove(propertyName);
201+
if (value == null) {
202+
throw newConfigurationException(processorType, processorTag, propertyName, "required property is missing");
203+
}
204+
try {
205+
return Double.parseDouble(value.toString());
206+
} catch (Exception e) {
207+
throw newConfigurationException(processorType, processorTag, propertyName,
208+
"property cannot be converted to a double [" + value.toString() + "]");
209+
}
210+
}
211+
192212
/**
193213
* Returns and removes the specified property of type list from the specified configuration map.
194214
*

x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/SpatialPlugin.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@
99
import org.elasticsearch.action.ActionResponse;
1010
import org.elasticsearch.common.settings.Settings;
1111
import org.elasticsearch.index.mapper.Mapper;
12+
import org.elasticsearch.ingest.Processor;
1213
import org.elasticsearch.plugins.ActionPlugin;
14+
import org.elasticsearch.plugins.IngestPlugin;
1315
import org.elasticsearch.plugins.MapperPlugin;
1416
import org.elasticsearch.plugins.Plugin;
1517
import org.elasticsearch.plugins.SearchPlugin;
1618
import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
1719
import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
1820
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
1921
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
22+
import org.elasticsearch.xpack.spatial.ingest.CircleProcessor;
2023

2124
import java.util.Arrays;
2225
import java.util.Collections;
@@ -26,7 +29,7 @@
2629

2730
import static java.util.Collections.singletonList;
2831

29-
public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin, SearchPlugin {
32+
public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin, SearchPlugin, IngestPlugin {
3033

3134
public SpatialPlugin(Settings settings) {
3235
}
@@ -49,4 +52,9 @@ public Map<String, Mapper.TypeParser> getMappers() {
4952
public List<QuerySpec<?>> getQueries() {
5053
return singletonList(new QuerySpec<>(ShapeQueryBuilder.NAME, ShapeQueryBuilder::new, ShapeQueryBuilder::fromXContent));
5154
}
55+
56+
@Override
57+
public Map<String, Processor.Factory> getProcessors(Processor.Parameters parameters) {
58+
return Map.of(CircleProcessor.TYPE, new CircleProcessor.Factory());
59+
}
5260
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.spatial;
7+
8+
import org.apache.lucene.util.SloppyMath;
9+
import org.elasticsearch.geometry.Circle;
10+
import org.elasticsearch.geometry.LinearRing;
11+
import org.elasticsearch.geometry.Polygon;
12+
import org.elasticsearch.index.mapper.GeoShapeIndexer;
13+
14+
/**
15+
* Utility class for storing different helpful re-usable spatial functions
16+
*/
17+
public class SpatialUtils {
18+
19+
private SpatialUtils() {}
20+
21+
/**
22+
* Makes an n-gon, centered at the provided circle's center, and each vertex approximately
23+
* {@link Circle#getRadiusMeters()} away from the center.
24+
*
25+
* This does not split the polygon across the date-line. Relies on {@link GeoShapeIndexer} to
26+
* split prepare polygon for indexing.
27+
*
28+
* Adapted from from org.apache.lucene.geo.GeoTestUtil
29+
* */
30+
public static Polygon createRegularGeoShapePolygon(Circle circle, int gons) {
31+
double[][] result = new double[2][];
32+
result[0] = new double[gons+1];
33+
result[1] = new double[gons+1];
34+
for(int i=0; i<gons; i++) {
35+
double angle = i * (360.0 / gons);
36+
double x = Math.cos(SloppyMath.toRadians(angle));
37+
double y = Math.sin(SloppyMath.toRadians(angle));
38+
double factor = 2.0;
39+
double step = 1.0;
40+
int last = 0;
41+
42+
// Iterate out along one spoke until we hone in on the point that's nearly exactly radiusMeters from the center:
43+
while (true) {
44+
double lat = circle.getLat() + y * factor;
45+
double lon = circle.getLon() + x * factor;
46+
double distanceMeters = SloppyMath.haversinMeters(circle.getLat(), circle.getLon(), lat, lon);
47+
48+
if (Math.abs(distanceMeters - circle.getRadiusMeters()) < 0.1) {
49+
// Within 10 cm: close enough!
50+
// lon/lat are left de-normalized so that indexing can properly detect dateline crossing.
51+
result[0][i] = lon;
52+
result[1][i] = lat;
53+
break;
54+
}
55+
56+
if (distanceMeters > circle.getRadiusMeters()) {
57+
// too big
58+
factor -= step;
59+
if (last == 1) {
60+
step /= 2.0;
61+
}
62+
last = -1;
63+
} else if (distanceMeters < circle.getRadiusMeters()) {
64+
// too small
65+
factor += step;
66+
if (last == -1) {
67+
step /= 2.0;
68+
}
69+
last = 1;
70+
}
71+
}
72+
}
73+
74+
// close poly
75+
result[0][gons] = result[0][0];
76+
result[1][gons] = result[1][0];
77+
return new Polygon(new LinearRing(result[0], result[1]));
78+
}
79+
80+
/**
81+
* Makes an n-gon, centered at the provided circle's center. This assumes
82+
* distance measured in cartesian geometry.
83+
**/
84+
public static Polygon createRegularShapePolygon(Circle circle, int gons) {
85+
double[][] result = new double[2][];
86+
result[0] = new double[gons+1];
87+
result[1] = new double[gons+1];
88+
for(int i=0; i<gons; i++) {
89+
double angle = i * (360.0 / gons);
90+
double x = circle.getRadiusMeters() * Math.cos(SloppyMath.toRadians(angle));
91+
double y = circle.getRadiusMeters() * Math.sin(SloppyMath.toRadians(angle));
92+
93+
result[0][i] = x + circle.getX();
94+
result[1][i] = y + circle.getY();
95+
}
96+
// close poly
97+
result[0][gons] = result[0][0];
98+
result[1][gons] = result[1][0];
99+
return new Polygon(new LinearRing(result[0], result[1]));
100+
}
101+
}

0 commit comments

Comments
 (0)