Skip to content

Commit e81ef33

Browse files
authored
Introduce GeometryTree writer/reader (#42331)
The GeometryTree represent an Elastisearch Geometry object. This includes collections like MultiPoint and GeometryCollection. For the initial implementation, only polygons without holes are supported. In a follow-up PR, the GeometryTree will be the object that interacts with doc-value reading and writing.
1 parent 00f5a60 commit e81ef33

File tree

3 files changed

+345
-0
lines changed

3 files changed

+345
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.common.geo;
20+
21+
import org.apache.lucene.util.BytesRef;
22+
import org.elasticsearch.common.io.stream.ByteBufferStreamInput;
23+
import org.elasticsearch.geo.geometry.ShapeType;
24+
25+
import java.io.IOException;
26+
import java.nio.ByteBuffer;
27+
28+
/**
29+
* A tree reader.
30+
*
31+
* This class supports checking bounding box
32+
* relations against the serialized geometry tree.
33+
*/
34+
public class GeometryTreeReader {
35+
36+
private final BytesRef bytesRef;
37+
38+
public GeometryTreeReader(BytesRef bytesRef) {
39+
this.bytesRef = bytesRef;
40+
}
41+
42+
public boolean containedInOrCrosses(int minLon, int minLat, int maxLon, int maxLat) throws IOException {
43+
ByteBufferStreamInput input = new ByteBufferStreamInput(
44+
ByteBuffer.wrap(bytesRef.bytes, bytesRef.offset, bytesRef.length));
45+
boolean hasExtent = input.readBoolean();
46+
if (hasExtent) {
47+
int thisMinLon = input.readInt();
48+
int thisMinLat = input.readInt();
49+
int thisMaxLon = input.readInt();
50+
int thisMaxLat = input.readInt();
51+
52+
if (thisMinLat > maxLat || thisMaxLon < minLon || thisMaxLat < minLat || thisMinLon > maxLon) {
53+
return false; // tree and bbox-query are disjoint
54+
}
55+
56+
if (minLon <= thisMinLon && minLat <= thisMinLat && maxLon >= thisMaxLon && maxLat >= thisMaxLat) {
57+
return true; // bbox-query fully contains tree's extent.
58+
}
59+
}
60+
61+
int numTrees = input.readVInt();
62+
for (int i = 0; i < numTrees; i++) {
63+
ShapeType shapeType = input.readEnum(ShapeType.class);
64+
if (ShapeType.POLYGON.equals(shapeType)) {
65+
BytesRef treeRef = input.readBytesRef();
66+
EdgeTreeReader reader = new EdgeTreeReader(treeRef);
67+
if (reader.containedInOrCrosses(minLon, minLat, maxLon, maxLat)) {
68+
return true;
69+
}
70+
}
71+
}
72+
return false;
73+
}
74+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.common.geo;
20+
21+
import org.apache.lucene.util.BytesRef;
22+
import org.elasticsearch.common.io.stream.BytesStreamOutput;
23+
import org.elasticsearch.geo.geometry.Circle;
24+
import org.elasticsearch.geo.geometry.Geometry;
25+
import org.elasticsearch.geo.geometry.GeometryCollection;
26+
import org.elasticsearch.geo.geometry.GeometryVisitor;
27+
import org.elasticsearch.geo.geometry.Line;
28+
import org.elasticsearch.geo.geometry.LinearRing;
29+
import org.elasticsearch.geo.geometry.MultiLine;
30+
import org.elasticsearch.geo.geometry.MultiPoint;
31+
import org.elasticsearch.geo.geometry.MultiPolygon;
32+
import org.elasticsearch.geo.geometry.Point;
33+
import org.elasticsearch.geo.geometry.Polygon;
34+
import org.elasticsearch.geo.geometry.Rectangle;
35+
import org.elasticsearch.geo.geometry.ShapeType;
36+
37+
import java.io.IOException;
38+
import java.util.ArrayList;
39+
import java.util.List;
40+
41+
/**
42+
* This is a tree-writer that serializes the
43+
* appropriate tree structure for each type of
44+
* {@link Geometry} into a byte array.
45+
*/
46+
public class GeometryTreeWriter {
47+
48+
private final GeometryTreeBuilder builder;
49+
50+
GeometryTreeWriter(Geometry geometry) {
51+
builder = new GeometryTreeBuilder();
52+
geometry.visit(builder);
53+
}
54+
55+
public BytesRef toBytesRef() throws IOException {
56+
BytesStreamOutput output = new BytesStreamOutput();
57+
// only write a geometry extent for the tree if the tree
58+
// contains multiple sub-shapes
59+
boolean prependExtent = builder.shapeWriters.size() > 1;
60+
output.writeBoolean(prependExtent);
61+
if (prependExtent) {
62+
output.writeInt(builder.minLon);
63+
output.writeInt(builder.minLat);
64+
output.writeInt(builder.maxLon);
65+
output.writeInt(builder.maxLat);
66+
}
67+
output.writeVInt(builder.shapeWriters.size());
68+
for (EdgeTreeWriter writer : builder.shapeWriters) {
69+
output.writeEnum(ShapeType.POLYGON);
70+
output.writeBytesRef(writer.toBytesRef());
71+
}
72+
output.close();
73+
return output.bytes().toBytesRef();
74+
}
75+
76+
class GeometryTreeBuilder implements GeometryVisitor<Void, RuntimeException> {
77+
78+
private List<EdgeTreeWriter> shapeWriters;
79+
// integers are used to represent int-encoded lat/lon values
80+
int minLat;
81+
int maxLat;
82+
int minLon;
83+
int maxLon;
84+
85+
GeometryTreeBuilder() {
86+
shapeWriters = new ArrayList<>();
87+
minLat = minLon = Integer.MAX_VALUE;
88+
maxLat = maxLon = Integer.MIN_VALUE;
89+
}
90+
91+
private void addWriter(EdgeTreeWriter writer) {
92+
minLon = Math.min(minLon, writer.minX);
93+
minLat = Math.min(minLat, writer.minY);
94+
maxLon = Math.max(maxLon, writer.maxX);
95+
maxLat = Math.max(maxLat, writer.maxY);
96+
shapeWriters.add(writer);
97+
}
98+
99+
@Override
100+
public Void visit(GeometryCollection<?> collection) {
101+
for (Geometry geometry : collection) {
102+
geometry.visit(this);
103+
}
104+
return null;
105+
}
106+
107+
@Override
108+
public Void visit(Line line) {
109+
throw new UnsupportedOperationException("support for Line is a TODO");
110+
}
111+
112+
@Override
113+
public Void visit(MultiLine multiLine) {
114+
for (Line line : multiLine) {
115+
visit(line);
116+
}
117+
return null;
118+
}
119+
120+
@Override
121+
public Void visit(Polygon polygon) {
122+
// TODO (support holes)
123+
LinearRing outerShell = polygon.getPolygon();
124+
addWriter(new EdgeTreeWriter(asIntArray(outerShell.getLons()), asIntArray(outerShell.getLats())));
125+
return null;
126+
}
127+
128+
@Override
129+
public Void visit(MultiPolygon multiPolygon) {
130+
for (Polygon polygon : multiPolygon) {
131+
visit(polygon);
132+
}
133+
return null;
134+
}
135+
136+
@Override
137+
public Void visit(Rectangle r) {
138+
int[] lats = new int[] { (int) r.getMinLat(), (int) r.getMinLat(), (int) r.getMaxLat(), (int) r.getMaxLat(),
139+
(int) r.getMinLat()};
140+
int[] lons = new int[] { (int) r.getMinLon(), (int) r.getMaxLon(), (int) r.getMaxLon(), (int) r.getMinLon(),
141+
(int) r.getMinLon()};
142+
addWriter(new EdgeTreeWriter(lons, lats));
143+
return null;
144+
}
145+
146+
@Override
147+
public Void visit(Point point) {
148+
throw new UnsupportedOperationException("support for Point is a TODO");
149+
}
150+
151+
@Override
152+
public Void visit(MultiPoint multiPoint) {
153+
throw new UnsupportedOperationException("support for MultiPoint is a TODO");
154+
}
155+
156+
@Override
157+
public Void visit(LinearRing ring) {
158+
throw new IllegalArgumentException("invalid shape type found [Circle]");
159+
}
160+
161+
@Override
162+
public Void visit(Circle circle) {
163+
throw new IllegalArgumentException("invalid shape type found [Circle]");
164+
}
165+
166+
private int[] asIntArray(double[] doub) {
167+
int[] intArr = new int[doub.length];
168+
for (int i = 0; i < intArr.length; i++) {
169+
intArr[i] = (int) doub[i];
170+
}
171+
return intArr;
172+
}
173+
}
174+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.common.geo;
20+
21+
import org.apache.lucene.util.BytesRef;
22+
import org.elasticsearch.geo.geometry.LinearRing;
23+
import org.elasticsearch.geo.geometry.Polygon;
24+
import org.elasticsearch.test.ESTestCase;
25+
26+
import java.io.IOException;
27+
import java.util.Collections;
28+
29+
public class GeometryTreeTests extends ESTestCase {
30+
31+
public void testRectangleShape() throws IOException {
32+
for (int i = 0; i < 1000; i++) {
33+
int minX = randomIntBetween(-180, 170);
34+
int maxX = randomIntBetween(minX + 10, 180);
35+
int minY = randomIntBetween(-90, 80);
36+
int maxY = randomIntBetween(minY + 10, 90);
37+
double[] x = new double[]{minX, maxX, maxX, minX, minX};
38+
double[] y = new double[]{minY, minY, maxY, maxY, minY};
39+
GeometryTreeWriter writer = new GeometryTreeWriter(new Polygon(new LinearRing(y, x), Collections.emptyList()));
40+
BytesRef bytes = writer.toBytesRef();
41+
GeometryTreeReader reader = new GeometryTreeReader(bytes);
42+
43+
// box-query touches bottom-left corner
44+
assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180), minX, minY));
45+
// box-query touches bottom-right corner
46+
assertTrue(reader.containedInOrCrosses(maxX, minY - randomIntBetween(1, 180), maxX + randomIntBetween(1, 180), minY));
47+
// box-query touches top-right corner
48+
assertTrue(reader.containedInOrCrosses(maxX, maxY, maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180)));
49+
// box-query touches top-left corner
50+
assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), maxY, minX, maxY + randomIntBetween(1, 180)));
51+
// box-query fully-enclosed inside rectangle
52+
assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, (3 * maxX + minX) / 4,
53+
(3 * maxY + minY) / 4));
54+
// box-query fully-contains poly
55+
assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 180), minY - randomIntBetween(1, 180),
56+
maxX + randomIntBetween(1, 180), maxY + randomIntBetween(1, 180)));
57+
// box-query half-in-half-out-right
58+
assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000),
59+
(3 * maxY + minY) / 4));
60+
// box-query half-in-half-out-left
61+
assertTrue(reader.containedInOrCrosses(minX - randomIntBetween(1, 1000), (3 * minY + maxY) / 4, (3 * maxX + minX) / 4,
62+
(3 * maxY + minY) / 4));
63+
// box-query half-in-half-out-top
64+
assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, (3 * minY + maxY) / 4, maxX + randomIntBetween(1, 1000),
65+
maxY + randomIntBetween(1, 1000)));
66+
// box-query half-in-half-out-bottom
67+
assertTrue(reader.containedInOrCrosses((3 * minX + maxX) / 4, minY - randomIntBetween(1, 1000),
68+
maxX + randomIntBetween(1, 1000), (3 * maxY + minY) / 4));
69+
70+
// box-query outside to the right
71+
assertFalse(reader.containedInOrCrosses(maxX + randomIntBetween(1, 1000), minY, maxX + randomIntBetween(1001, 2000), maxY));
72+
// box-query outside to the left
73+
assertFalse(reader.containedInOrCrosses(maxX - randomIntBetween(1001, 2000), minY, minX - randomIntBetween(1, 1000), maxY));
74+
// box-query outside to the top
75+
assertFalse(reader.containedInOrCrosses(minX, maxY + randomIntBetween(1, 1000), maxX, maxY + randomIntBetween(1001, 2000)));
76+
// box-query outside to the bottom
77+
assertFalse(reader.containedInOrCrosses(minX, minY - randomIntBetween(1001, 2000), maxX, minY - randomIntBetween(1, 1000)));
78+
}
79+
}
80+
81+
public void testPacMan() throws Exception {
82+
// pacman
83+
double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0};
84+
double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0};
85+
86+
// candidate containedInOrCrosses cell
87+
int xMin = 2;//-5;
88+
int xMax = 11;//0.000001;
89+
int yMin = -1;//0;
90+
int yMax = 1;//5;
91+
92+
// test cell crossing poly
93+
GeometryTreeWriter writer = new GeometryTreeWriter(new Polygon(new LinearRing(py, px), Collections.emptyList()));
94+
GeometryTreeReader reader = new GeometryTreeReader(writer.toBytesRef());
95+
assertTrue(reader.containedInOrCrosses(xMin, yMin, xMax, yMax));
96+
}
97+
}

0 commit comments

Comments
 (0)