Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Circle Processor #43851

Merged
merged 28 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d8108ed
add circle-processor that translates circles to polygons
talevy Aug 15, 2019
dea5efe
updates
talevy Aug 20, 2019
63f3295
Merge remote-tracking branch 'elastic/master' into circleprocessor
talevy Aug 20, 2019
3d233da
remove common options from docs table
talevy Aug 20, 2019
a84b230
moar table cleanup
talevy Aug 20, 2019
bfd46eb
add docs to root ingest-node processor list
talevy Aug 21, 2019
3cd1133
add image describing error_distance_in_meters
talevy Aug 22, 2019
0a03055
Merge remote-tracking branch 'elastic/master' into circleprocessor
talevy Aug 22, 2019
7686e61
update image
talevy Aug 22, 2019
7da99a4
Merge remote-tracking branch 'elastic/master' into circleprocessor
talevy Aug 22, 2019
53d7277
add dateline intersection tests
talevy Aug 23, 2019
34f2ba7
fix across dateline
talevy Aug 23, 2019
7fd89db
fix bugs
talevy Aug 23, 2019
56df880
add more docs to createRegularPolygon
talevy Aug 23, 2019
464f3b2
more cleanup
talevy Aug 23, 2019
80bfa6c
fix test
talevy Aug 23, 2019
c96504c
fix final docs
talevy Aug 23, 2019
8057cf5
fix checkstyle
talevy Aug 23, 2019
41c23be
add support for shape type in addition to geo_shape type
talevy Aug 26, 2019
42ed477
Merge remote-tracking branch 'elastic/master' into circleprocessor
talevy Aug 26, 2019
b8594d9
add test for shape-query
talevy Aug 26, 2019
68373cf
cleanup test
talevy Aug 26, 2019
bae807a
remove println
talevy Aug 26, 2019
15b4239
Merge remote-tracking branch 'elastic/master' into circleprocessor
talevy Aug 26, 2019
6eb73a6
respond to review and discussion
talevy Aug 28, 2019
95aa218
Merge remote-tracking branch 'elastic/master' into circleprocessor
talevy Aug 28, 2019
519e39c
remove unused import
talevy Aug 28, 2019
4ac2a7e
respond to review
talevy Aug 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/reference/images/spatial/error_distance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/reference/ingest/ingest-node.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ See {plugins}/ingest.html[Ingest plugins] for information about the available in

include::processors/append.asciidoc[]
include::processors/bytes.asciidoc[]
include::processors/circle.asciidoc[]
include::processors/convert.asciidoc[]
include::processors/date.asciidoc[]
include::processors/date-index-name.asciidoc[]
Expand Down
165 changes: 165 additions & 0 deletions docs/reference/ingest/processors/circle.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
[role="xpack"]
[testenv="basic"]
[[ingest-circle-processor]]
=== Circle Processor
Converts circle definitions of shapes to regular polygons which approximate them.

[[circle-processor-options]]
.Circle Processor Options
talevy marked this conversation as resolved.
Show resolved Hide resolved
[options="header"]
|======
| Name | Required | Default | Description
| `field` | yes | - | The string-valued field to trim whitespace from
| `target_field` | no | `field` | The field to assign the polygon shape to, by default `field` is updated in-place
| `ignore_missing` | no | `false` | If `true` and `field` does not exist, the processor quietly exits without modifying the document
| `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`)
| `shape_type` | yes | - | which field mapping type is to be used when processing the circle: `geo_shape` or `shape`
include::common-options.asciidoc[]
|======


image:images/spatial/error_distance.png[]

[source,js]
--------------------------------------------------
PUT circles
{
"mappings": {
"properties": {
"circle": {
"type": "geo_shape"
}
}
}
}

PUT _ingest/pipeline/polygonize_circles
{
"description": "translate circle to polygon",
"processors": [
{
"circle": {
"field": "circle",
"error_distance": 28.0,
"shape_type": "geo_shape"
}
}
]
}
--------------------------------------------------
// CONSOLE

Using the above pipeline, we can attempt to index a document into the `circles` index.
The circle can be represented as either a WKT circle or a GeoJSON circle. The resulting
polygon will be represented and indexed using the same format as the input circle. WKT will
be translated to a WKT polygon, and GeoJSON circles will be translated to GeoJSON polygons.

==== Example: Circle defined in Well Known Text

In this example a circle defined in WKT format is indexed

[source,js]
--------------------------------------------------
PUT circles/_doc/1?pipeline=polygonize_circles
{
"circle": "CIRCLE (30 10 40)"
}

GET circles/_doc/1
--------------------------------------------------
// CONSOLE
// TEST[continued]

The response from the above index request:

[source,js]
--------------------------------------------------
{
"found": true,
"_index": "circles",
"_type": "_doc",
"_id": "1",
"_version": 1,
"_seq_no": 22,
"_primary_term": 1,
"_source": {
"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))"
}
}
--------------------------------------------------
// TESTRESPONSE[s/"_seq_no": \d+/"_seq_no" : $body._seq_no/ s/"_primary_term": 1/"_primary_term" : $body._primary_term/]

==== Example: Circle defined in GeoJSON

In this example a circle defined in GeoJSON format is indexed

[source,js]
--------------------------------------------------
PUT circles/_doc/2?pipeline=polygonize_circles
{
"circle": {
"type": "circle",
"radius": "40m",
"coordinates": [30, 10]
}
}

GET circles/_doc/2
--------------------------------------------------
// CONSOLE
// TEST[continued]

The response from the above index request:

[source,js]
--------------------------------------------------
{
"found": true,
"_index": "circles",
"_type": "_doc",
"_id": "2",
"_version": 1,
"_seq_no": 22,
"_primary_term": 1,
"_source": {
"circle": {
"coordinates": [
[
[30.000365257263184, 10.0],
[30.000111397193788, 10.00034284530941],
[29.999706043744222, 10.000213571721195],
[29.999706043744222, 9.999786428278805],
[30.000111397193788, 9.99965715469059],
[30.000365257263184, 10.0]
]
],
"type": "polygon"
}
}
}
--------------------------------------------------
// TESTRESPONSE[s/"_seq_no": \d+/"_seq_no" : $body._seq_no/ s/"_primary_term": 1/"_primary_term" : $body._primary_term/]


==== Notes on Accuracy

Accuracy of the polygon that represents the circle is defined as `error_distance`. The smaller this
difference is, the closer to a perfect circle the polygon is.

Below is a table that aims to help capture how the radius of the circle affects the resulting number of sides
of the polygon given different inputs.

The minimum number of sides is `4` and the maximum is `1000`.

[[circle-processor-accuracy]]
.Circle Processor Accuracy
[options="header"]
|======
| error_distance | radius in meters | number of sides of polygon
| 1.00 | 1.0 | 4
| 1.00 | 10.0 | 14
| 1.00 | 100.0 | 45
| 1.00 | 1000.0 | 141
| 1.00 | 10000.0 | 445
| 1.00 | 100000.0 | 1000
|======
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,26 @@ public static Integer readIntProperty(String processorType, String processorTag,
}
}

/**
* Returns and removes the specified property from the specified configuration map.
*
* If the property value isn't of type int a {@link ElasticsearchParseException} is thrown.
* If the property is missing an {@link ElasticsearchParseException} is thrown
*/
public static Double readDoubleProperty(String processorType, String processorTag, Map<String, Object> configuration,
String propertyName) {
Object value = configuration.remove(propertyName);
if (value == null) {
throw newConfigurationException(processorType, processorTag, propertyName, "required property is missing");
}
try {
return Double.parseDouble(value.toString());
} catch (Exception e) {
throw newConfigurationException(processorType, processorTag, propertyName,
"property cannot be converted to a double [" + value.toString() + "]");
}
}

/**
* Returns and removes the specified property of type list from the specified configuration map.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.mapper.Mapper;
import org.elasticsearch.ingest.Processor;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.IngestPlugin;
import org.elasticsearch.plugins.MapperPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.SearchPlugin;
import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper;
import org.elasticsearch.xpack.spatial.index.query.ShapeQueryBuilder;
import org.elasticsearch.xpack.spatial.ingest.CircleProcessor;

import java.util.Arrays;
import java.util.Collections;
Expand All @@ -26,7 +29,7 @@

import static java.util.Collections.singletonList;

public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin, SearchPlugin {
public class SpatialPlugin extends Plugin implements ActionPlugin, MapperPlugin, SearchPlugin, IngestPlugin {

public SpatialPlugin(Settings settings) {
}
Expand All @@ -49,4 +52,9 @@ public Map<String, Mapper.TypeParser> getMappers() {
public List<QuerySpec<?>> getQueries() {
return singletonList(new QuerySpec<>(ShapeQueryBuilder.NAME, ShapeQueryBuilder::new, ShapeQueryBuilder::fromXContent));
}

@Override
public Map<String, Processor.Factory> getProcessors(Processor.Parameters parameters) {
return Map.of(CircleProcessor.TYPE, new CircleProcessor.Factory());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.spatial;

import org.apache.lucene.util.SloppyMath;
import org.elasticsearch.geometry.Circle;
import org.elasticsearch.geometry.LinearRing;
import org.elasticsearch.geometry.Polygon;
import org.elasticsearch.index.mapper.GeoShapeIndexer;

/**
* Utility class for storing different helpful re-usable spatial functions
*/
public class SpatialUtils {

private SpatialUtils() {}

/**
* Makes an n-gon, centered at the provided circle's center, and each vertex approximately
* {@link Circle#getRadiusMeters()} away from the center.
*
* This does not split the polygon across the date-line. Relies on {@link GeoShapeIndexer} to
* split prepare polygon for indexing.
*
* Adapted from from org.apache.lucene.geo.GeoTestUtil
* */
public static Polygon createRegularGeoShapePolygon(Circle circle, int gons) {
double[][] result = new double[2][];
result[0] = new double[gons+1];
result[1] = new double[gons+1];
for(int i=0; i<gons; i++) {
double angle = i * (360.0 / gons);
double x = Math.cos(SloppyMath.toRadians(angle));
double y = Math.sin(SloppyMath.toRadians(angle));
double factor = 2.0;
double step = 1.0;
int last = 0;

// Iterate out along one spoke until we hone in on the point that's nearly exactly radiusMeters from the center:
while (true) {
double lat = circle.getLat() + y * factor;
double lon = circle.getLon() + x * factor;
double distanceMeters = SloppyMath.haversinMeters(circle.getLat(), circle.getLon(), lat, lon);

if (Math.abs(distanceMeters - circle.getRadiusMeters()) < 0.1) {
// Within 10 cm: close enough!
// lon/lat are left de-normalized so that indexing can properly detect dateline crossing.
result[0][i] = lon;
result[1][i] = lat;
break;
}

if (distanceMeters > circle.getRadiusMeters()) {
// too big
factor -= step;
if (last == 1) {
step /= 2.0;
}
last = -1;
} else if (distanceMeters < circle.getRadiusMeters()) {
// too small
factor += step;
if (last == -1) {
step /= 2.0;
}
last = 1;
}
}
}

// close poly
result[0][gons] = result[0][0];
result[1][gons] = result[1][0];
return new Polygon(new LinearRing(result[0], result[1]));
}

/**
* Makes an n-gon, centered at the provided circle's center. This assumes
* distance measured in cartesian geometry.
**/
public static Polygon createRegularShapePolygon(Circle circle, int gons) {
double[][] result = new double[2][];
result[0] = new double[gons+1];
result[1] = new double[gons+1];
for(int i=0; i<gons; i++) {
double angle = i * (360.0 / gons);
double x = circle.getRadiusMeters() * Math.cos(SloppyMath.toRadians(angle));
double y = circle.getRadiusMeters() * Math.sin(SloppyMath.toRadians(angle));

result[0][i] = x + circle.getX();
result[1][i] = y + circle.getY();
}
// close poly
result[0][gons] = result[0][0];
result[1][gons] = result[1][0];
return new Polygon(new LinearRing(result[0], result[1]));
}
}
Loading