Skip to content

Commit

Permalink
Add Circle Processor (#43851)
Browse files Browse the repository at this point in the history
add circle-processor that translates circles to polygons
  • Loading branch information
talevy authored Aug 28, 2019
1 parent 8d16c9b commit e1c060a
Show file tree
Hide file tree
Showing 10 changed files with 898 additions and 1 deletion.
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
[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

0 comments on commit e1c060a

Please sign in to comment.