From 6fc0b83e0746e98feb07307ddccace864e7ff18a Mon Sep 17 00:00:00 2001 From: Chris Male Date: Thu, 20 Sep 2012 22:25:09 +1200 Subject: [PATCH] Upgraded to Spatial4j 0.3 --- pom.xml | 2 +- .../common/geo/GeoJSONShapeParser.java | 12 ++-- .../common/geo/GeoJSONShapeSerializer.java | 2 +- .../common/geo/GeoShapeConstants.java | 30 +++++++++ .../common/geo/ShapeBuilder.java | 20 +++--- .../common/geo/ShapesAvailability.java | 4 +- .../lucene/spatial/SpatialStrategy.java | 33 +++++++++- .../prefix/TermQueryPrefixTreeStrategy.java | 22 +++---- .../prefix/tree/GeohashPrefixTree.java | 16 +++-- .../lucene/spatial/prefix/tree/Node.java | 4 +- .../spatial/prefix/tree/QuadPrefixTree.java | 64 ++++++++++--------- .../prefix/tree/SpatialPrefixTree.java | 38 +++-------- .../index/mapper/geo/GeoShapeFieldMapper.java | 11 +--- .../common/geo/GeoJSONShapeParserTests.java | 11 ++-- .../geo/GeoJSONShapeSerializerTests.java | 11 ++-- .../TermQueryPrefixTreeStrategyTests.java | 9 +-- 16 files changed, 168 insertions(+), 121 deletions(-) create mode 100644 src/main/java/org/elasticsearch/common/geo/GeoShapeConstants.java diff --git a/pom.xml b/pom.xml index 5bc41568ae3..b844c24d030 100644 --- a/pom.xml +++ b/pom.xml @@ -155,7 +155,7 @@ com.spatial4j spatial4j - 0.2 + 0.3 compile true diff --git a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java index 7dafbfdbad7..daceb2f12de 100644 --- a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java +++ b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeParser.java @@ -20,9 +20,9 @@ package org.elasticsearch.common.geo; import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.impl.RectangleImpl; import com.spatial4j.core.shape.jts.JtsGeometry; import com.spatial4j.core.shape.jts.JtsPoint; -import com.spatial4j.core.shape.simple.RectangleImpl; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LinearRing; @@ -138,9 +138,9 @@ public class GeoJSONShapeParser { */ private static Shape buildShape(String shapeType, CoordinateNode node) { if ("point".equals(shapeType)) { - return new JtsPoint(GEOMETRY_FACTORY.createPoint(node.coordinate)); + return new JtsPoint(GEOMETRY_FACTORY.createPoint(node.coordinate), GeoShapeConstants.SPATIAL_CONTEXT); } else if ("linestring".equals(shapeType)) { - return new JtsGeometry(GEOMETRY_FACTORY.createLineString(toCoordinates(node))); + return new JtsGeometry(GEOMETRY_FACTORY.createLineString(toCoordinates(node)), GeoShapeConstants.SPATIAL_CONTEXT, true); } else if ("polygon".equals(shapeType)) { LinearRing shell = GEOMETRY_FACTORY.createLinearRing(toCoordinates(node.children.get(0))); LinearRing[] holes = null; @@ -150,12 +150,12 @@ public class GeoJSONShapeParser { holes[i] = GEOMETRY_FACTORY.createLinearRing(toCoordinates(node.children.get(i + 1))); } } - return new JtsGeometry(GEOMETRY_FACTORY.createPolygon(shell, holes)); + return new JtsGeometry(GEOMETRY_FACTORY.createPolygon(shell, holes), GeoShapeConstants.SPATIAL_CONTEXT, true); } else if ("multipoint".equals(shapeType)) { - return new JtsGeometry(GEOMETRY_FACTORY.createMultiPoint(toCoordinates(node))); + return new JtsGeometry(GEOMETRY_FACTORY.createMultiPoint(toCoordinates(node)), GeoShapeConstants.SPATIAL_CONTEXT, true); } else if ("envelope".equals(shapeType)) { Coordinate[] coordinates = toCoordinates(node); - return new RectangleImpl(coordinates[0].x, coordinates[1].x, coordinates[1].y, coordinates[0].y); + return new RectangleImpl(coordinates[0].x, coordinates[1].x, coordinates[1].y, coordinates[0].y, GeoShapeConstants.SPATIAL_CONTEXT); } throw new UnsupportedOperationException("ShapeType [" + shapeType + "] not supported"); diff --git a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java index 34843da22b5..e95df6fec84 100644 --- a/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java +++ b/src/main/java/org/elasticsearch/common/geo/GeoJSONShapeSerializer.java @@ -50,7 +50,7 @@ public class GeoJSONShapeSerializer { */ public static void serialize(Shape shape, XContentBuilder builder) throws IOException { if (shape instanceof JtsGeometry) { - Geometry geometry = ((JtsGeometry) shape).geo; + Geometry geometry = ((JtsGeometry) shape).getGeom(); if (geometry instanceof Point) { serializePoint((Point) geometry, builder); } else if (geometry instanceof LineString) { diff --git a/src/main/java/org/elasticsearch/common/geo/GeoShapeConstants.java b/src/main/java/org/elasticsearch/common/geo/GeoShapeConstants.java new file mode 100644 index 00000000000..355ffa4af50 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/geo/GeoShapeConstants.java @@ -0,0 +1,30 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.geo; + +import com.spatial4j.core.context.jts.JtsSpatialContext; + +/** + * Common constants through the GeoShape codebase + */ +public interface GeoShapeConstants { + + public static final JtsSpatialContext SPATIAL_CONTEXT = new JtsSpatialContext(true); +} diff --git a/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java b/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java index 256dc8ef274..33e50460166 100644 --- a/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java +++ b/src/main/java/org/elasticsearch/common/geo/ShapeBuilder.java @@ -22,10 +22,10 @@ package org.elasticsearch.common.geo; import com.spatial4j.core.shape.Point; import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; +import com.spatial4j.core.shape.impl.PointImpl; +import com.spatial4j.core.shape.impl.RectangleImpl; import com.spatial4j.core.shape.jts.JtsGeometry; import com.spatial4j.core.shape.jts.JtsPoint; -import com.spatial4j.core.shape.simple.PointImpl; -import com.spatial4j.core.shape.simple.RectangleImpl; import com.vividsolutions.jts.geom.*; import java.util.ArrayList; @@ -50,7 +50,7 @@ public class ShapeBuilder { * @return Point with the latitude and longitude */ public static Point newPoint(double lon, double lat) { - return new PointImpl(lon, lat); + return new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT); } /** @@ -80,9 +80,9 @@ public class ShapeBuilder { */ public static Geometry toJTSGeometry(Shape shape) { if (shape instanceof JtsGeometry) { - return ((JtsGeometry) shape).geo; + return ((JtsGeometry) shape).getGeom(); } else if (shape instanceof JtsPoint) { - return ((JtsPoint) shape).getJtsPoint(); + return ((JtsPoint) shape).getGeom(); } else if (shape instanceof Rectangle) { Rectangle rectangle = (Rectangle) shape; @@ -119,7 +119,7 @@ public class ShapeBuilder { * @return this */ public RectangleBuilder topLeft(double lon, double lat) { - this.topLeft = new PointImpl(lon, lat); + this.topLeft = new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT); return this; } @@ -131,7 +131,7 @@ public class ShapeBuilder { * @return this */ public RectangleBuilder bottomRight(double lon, double lat) { - this.bottomRight = new PointImpl(lon, lat); + this.bottomRight = new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT); return this; } @@ -141,7 +141,7 @@ public class ShapeBuilder { * @return Built Rectangle */ public Rectangle build() { - return new RectangleImpl(topLeft.getX(), bottomRight.getX(), bottomRight.getY(), topLeft.getY()); + return new RectangleImpl(topLeft.getX(), bottomRight.getX(), bottomRight.getY(), topLeft.getY(), GeoShapeConstants.SPATIAL_CONTEXT); } } @@ -160,7 +160,7 @@ public class ShapeBuilder { * @return this */ public PolygonBuilder point(double lon, double lat) { - points.add(new PointImpl(lon, lat)); + points.add(new PointImpl(lon, lat, GeoShapeConstants.SPATIAL_CONTEXT)); return this; } @@ -170,7 +170,7 @@ public class ShapeBuilder { * @return Built polygon */ public Shape build() { - return new JtsGeometry(toPolygon()); + return new JtsGeometry(toPolygon(), GeoShapeConstants.SPATIAL_CONTEXT, true); } /** diff --git a/src/main/java/org/elasticsearch/common/geo/ShapesAvailability.java b/src/main/java/org/elasticsearch/common/geo/ShapesAvailability.java index bef043d5d77..0ce2087f5f3 100644 --- a/src/main/java/org/elasticsearch/common/geo/ShapesAvailability.java +++ b/src/main/java/org/elasticsearch/common/geo/ShapesAvailability.java @@ -19,7 +19,7 @@ package org.elasticsearch.common.geo; -import com.spatial4j.core.shape.simple.PointImpl; +import com.spatial4j.core.shape.impl.PointImpl; import com.vividsolutions.jts.geom.GeometryFactory; /** @@ -32,7 +32,7 @@ public class ShapesAvailability { static { boolean xSPATIAL4J_AVAILABLE; try { - new PointImpl(0, 0); + new PointImpl(0, 0, GeoShapeConstants.SPATIAL_CONTEXT); xSPATIAL4J_AVAILABLE = true; } catch (Throwable t) { xSPATIAL4J_AVAILABLE = false; diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/SpatialStrategy.java b/src/main/java/org/elasticsearch/common/lucene/spatial/SpatialStrategy.java index a2c90834ed7..5ee2110402f 100644 --- a/src/main/java/org/elasticsearch/common/lucene/spatial/SpatialStrategy.java +++ b/src/main/java/org/elasticsearch/common/lucene/spatial/SpatialStrategy.java @@ -1,15 +1,18 @@ package org.elasticsearch.common.lucene.spatial; +import com.spatial4j.core.context.SpatialContext; +import com.spatial4j.core.shape.Point; +import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; import org.apache.lucene.document.Field; import org.apache.lucene.document.Fieldable; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.GeoShapeConstants; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.lucene.spatial.prefix.NodeTokenStream; import org.elasticsearch.common.lucene.spatial.prefix.tree.Node; import org.elasticsearch.common.lucene.spatial.prefix.tree.SpatialPrefixTree; -import org.elasticsearch.index.cache.filter.FilterCache; import org.elasticsearch.index.mapper.FieldMapper; import java.util.List; @@ -54,7 +57,8 @@ public abstract class SpatialStrategy { * @return Fieldable for indexing the Shape */ public Fieldable createField(Shape shape) { - int detailLevel = prefixTree.getMaxLevelForPrecision(shape, distanceErrorPct); + int detailLevel = prefixTree.getLevelForDistance( + calcDistanceFromErrPct(shape, distanceErrorPct, GeoShapeConstants.SPATIAL_CONTEXT)); List nodes = prefixTree.getNodes(shape, detailLevel, true); NodeTokenStream tokenStream = nodeTokenStream.get(); tokenStream.setNodes(nodes); @@ -185,4 +189,29 @@ public abstract class SpatialStrategy { public SpatialPrefixTree getPrefixTree() { return prefixTree; } + + /** + * Computes the distance given a shape and the {@code distErrPct}. The + * algorithm is the fraction of the distance from the center of the query + * shape to its furthest bounding box corner. + * + * @param shape Mandatory. + * @param distErrPct 0 to 0.5 + * @param ctx Mandatory + * @return A distance (in degrees). + */ + protected final double calcDistanceFromErrPct(Shape shape, double distErrPct, SpatialContext ctx) { + if (distErrPct < 0 || distErrPct > 0.5) { + throw new IllegalArgumentException("distErrPct " + distErrPct + " must be between [0 to 0.5]"); + } + if (distErrPct == 0 || shape instanceof Point) { + return 0; + } + Rectangle bbox = shape.getBoundingBox(); + //The diagonal distance should be the same computed from any opposite corner, + // and this is the longest distance that might be occurring within the shape. + double diagonalDist = ctx.getDistCalc().distance( + ctx.makePoint(bbox.getMinX(), bbox.getMinY()), bbox.getMaxX(), bbox.getMaxY()); + return diagonalDist * 0.5 * distErrPct; + } } diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategy.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategy.java index 70878c7ea22..4eb001d85a0 100644 --- a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategy.java +++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategy.java @@ -7,17 +7,15 @@ import com.vividsolutions.jts.operation.buffer.BufferOp; import com.vividsolutions.jts.operation.buffer.BufferParameters; import org.apache.lucene.index.Term; import org.apache.lucene.search.*; -import org.elasticsearch.common.lucene.search.OrFilter; +import org.elasticsearch.common.geo.GeoShapeConstants; import org.elasticsearch.common.lucene.search.TermFilter; import org.elasticsearch.common.lucene.search.XBooleanFilter; import org.elasticsearch.common.lucene.spatial.SpatialStrategy; import org.elasticsearch.common.lucene.spatial.prefix.tree.Node; import org.elasticsearch.common.lucene.spatial.prefix.tree.SpatialPrefixTree; import org.elasticsearch.common.geo.ShapeBuilder; -import org.elasticsearch.index.cache.filter.FilterCache; import org.elasticsearch.index.mapper.FieldMapper; -import java.util.ArrayList; import java.util.List; /** @@ -46,7 +44,8 @@ public class TermQueryPrefixTreeStrategy extends SpatialStrategy { */ @Override public Filter createIntersectsFilter(Shape shape) { - int detailLevel = getPrefixTree().getMaxLevelForPrecision(shape, getDistanceErrorPct()); + int detailLevel = getPrefixTree().getLevelForDistance( + calcDistanceFromErrPct(shape, getDistanceErrorPct(), GeoShapeConstants.SPATIAL_CONTEXT)); List nodes = getPrefixTree().getNodes(shape, detailLevel, false); Term[] nodeTerms = new Term[nodes.size()]; @@ -61,7 +60,8 @@ public class TermQueryPrefixTreeStrategy extends SpatialStrategy { */ @Override public Query createIntersectsQuery(Shape shape) { - int detailLevel = getPrefixTree().getMaxLevelForPrecision(shape, getDistanceErrorPct()); + int detailLevel = getPrefixTree().getLevelForDistance( + calcDistanceFromErrPct(shape, getDistanceErrorPct(), GeoShapeConstants.SPATIAL_CONTEXT)); List nodes = getPrefixTree().getNodes(shape, detailLevel, false); BooleanQuery query = new BooleanQuery(); @@ -78,7 +78,8 @@ public class TermQueryPrefixTreeStrategy extends SpatialStrategy { */ @Override public Filter createDisjointFilter(Shape shape) { - int detailLevel = getPrefixTree().getMaxLevelForPrecision(shape, getDistanceErrorPct()); + int detailLevel = getPrefixTree().getLevelForDistance( + calcDistanceFromErrPct(shape, getDistanceErrorPct(), GeoShapeConstants.SPATIAL_CONTEXT)); List nodes = getPrefixTree().getNodes(shape, detailLevel, false); XBooleanFilter filter = new XBooleanFilter(); @@ -94,7 +95,8 @@ public class TermQueryPrefixTreeStrategy extends SpatialStrategy { */ @Override public Query createDisjointQuery(Shape shape) { - int detailLevel = getPrefixTree().getMaxLevelForPrecision(shape, getDistanceErrorPct()); + int detailLevel = getPrefixTree().getLevelForDistance( + calcDistanceFromErrPct(shape, getDistanceErrorPct(), GeoShapeConstants.SPATIAL_CONTEXT)); List nodes = getPrefixTree().getNodes(shape, detailLevel, false); BooleanQuery query = new BooleanQuery(); @@ -115,10 +117,8 @@ public class TermQueryPrefixTreeStrategy extends SpatialStrategy { Filter intersectsFilter = createIntersectsFilter(shape); Geometry shapeGeometry = ShapeBuilder.toJTSGeometry(shape); - // TODO: Need some way to detect if having the buffer is going to push the shape over the dateline - // and throw an error in this instance Geometry buffer = BufferOp.bufferOp(shapeGeometry, CONTAINS_BUFFER_DISTANCE, BUFFER_PARAMETERS); - Shape bufferedShape = new JtsGeometry(buffer.difference(shapeGeometry)); + Shape bufferedShape = new JtsGeometry(buffer.difference(shapeGeometry), GeoShapeConstants.SPATIAL_CONTEXT, true); Filter bufferedFilter = createIntersectsFilter(bufferedShape); XBooleanFilter filter = new XBooleanFilter(); @@ -137,7 +137,7 @@ public class TermQueryPrefixTreeStrategy extends SpatialStrategy { Geometry shapeGeometry = ShapeBuilder.toJTSGeometry(shape); Geometry buffer = BufferOp.bufferOp(shapeGeometry, CONTAINS_BUFFER_DISTANCE, BUFFER_PARAMETERS); - Shape bufferedShape = new JtsGeometry(buffer.difference(shapeGeometry)); + Shape bufferedShape = new JtsGeometry(buffer.difference(shapeGeometry), GeoShapeConstants.SPATIAL_CONTEXT, true); Query bufferedQuery = createIntersectsQuery(bufferedShape); BooleanQuery query = new BooleanQuery(); diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/GeohashPrefixTree.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/GeohashPrefixTree.java index 3bf0019f6e8..c9fc79c0977 100644 --- a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/GeohashPrefixTree.java +++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/GeohashPrefixTree.java @@ -18,10 +18,10 @@ package org.elasticsearch.common.lucene.spatial.prefix.tree; import com.spatial4j.core.context.SpatialContext; +import com.spatial4j.core.io.GeohashUtils; import com.spatial4j.core.shape.Point; import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; -import com.spatial4j.core.util.GeohashUtils; import java.util.ArrayList; import java.util.Collection; @@ -30,6 +30,8 @@ import java.util.List; /** * A SpatialPrefixGrid based on Geohashes. Uses {@link GeohashUtils} to do all the geohash work. + * + * @lucene.experimental */ public class GeohashPrefixTree extends SpatialPrefixTree { @@ -37,19 +39,23 @@ public class GeohashPrefixTree extends SpatialPrefixTree { super(ctx, maxLevels); Rectangle bounds = ctx.getWorldBounds(); if (bounds.getMinX() != -180) - throw new IllegalArgumentException("Geohash only supports lat-lon world bounds. Got "+bounds); + throw new IllegalArgumentException("Geohash only supports lat-lon world bounds. Got " + bounds); int MAXP = getMaxLevelsPossible(); if (maxLevels <= 0 || maxLevels > MAXP) - throw new IllegalArgumentException("maxLen must be [1-"+MAXP+"] but got "+ maxLevels); + throw new IllegalArgumentException("maxLen must be [1-" + MAXP + "] but got " + maxLevels); } - /** Any more than this and there's no point (double lat & lon are the same). */ + /** + * Any more than this and there's no point (double lat & lon are the same). + */ public static int getMaxLevelsPossible() { return GeohashUtils.MAX_PRECISION; } @Override public int getLevelForDistance(double dist) { + if (dist == 0) + return maxLevels;//short circuit final int level = GeohashUtils.lookupHashLenForWidthHeight(dist, dist); return Math.max(Math.min(level, maxLevels), 1); } @@ -107,7 +113,7 @@ public class GeohashPrefixTree extends SpatialPrefixTree { @Override public Node getSubCell(Point p) { - return GeohashPrefixTree.this.getNode(p,getLevel()+1);//not performant! + return GeohashPrefixTree.this.getNode(p, getLevel() + 1);//not performant! } private Shape shape;//cache diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/Node.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/Node.java index c22acf6d5b2..f8ff8c8037e 100644 --- a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/Node.java +++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/Node.java @@ -28,6 +28,8 @@ import java.util.List; /** * Represents a grid cell. These are not necessarily threadsafe, although new Cell("") (world cell) must be. + * + * @lucene.experimental */ public abstract class Node implements Comparable { public static final byte LEAF_BYTE = '+';//NOTE: must sort before letters & numbers @@ -150,7 +152,7 @@ public abstract class Node implements Comparable { } List copy = new ArrayList(cells.size());//copy since cells contractually isn't modifiable for (Node cell : cells) { - SpatialRelation rel = cell.getShape().relate(shapeFilter, spatialPrefixTree.ctx); + SpatialRelation rel = cell.getShape().relate(shapeFilter); if (rel == SpatialRelation.DISJOINT) continue; cell.shapeRel = rel; diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/QuadPrefixTree.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/QuadPrefixTree.java index 6dc6dcb3367..01678ca8b4f 100644 --- a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/QuadPrefixTree.java +++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/QuadPrefixTree.java @@ -22,14 +22,17 @@ import com.spatial4j.core.shape.Point; import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; import com.spatial4j.core.shape.SpatialRelation; -import com.spatial4j.core.shape.simple.PointImpl; +import java.io.PrintStream; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Locale; - +/** + * @lucene.experimental + */ public class QuadPrefixTree extends SpatialPrefixTree { public static final int MAX_LEVELS_POSSIBLE = 50;//not really sure how big this should be @@ -47,8 +50,8 @@ public class QuadPrefixTree extends SpatialPrefixTree { final double[] levelW; final double[] levelH; - final int[] levelS; // side - final int[] levelN; // number + final int[] levelS; // side + final int[] levelN; // number public QuadPrefixTree( SpatialContext ctx, Rectangle bounds, int maxLevels) { @@ -65,10 +68,10 @@ public class QuadPrefixTree extends SpatialPrefixTree { gridW = xmax - xmin; gridH = ymax - ymin; - this.xmid = xmin + gridW/2.0; - this.ymid = ymin + gridH/2.0; - levelW[0] = gridW/2.0; - levelH[0] = gridH/2.0; + this.xmid = xmin + gridW / 2.0; + this.ymid = ymin + gridH / 2.0; + levelW[0] = gridW / 2.0; + levelH[0] = gridH / 2.0; levelS[0] = 2; levelN[0] = 4; @@ -89,24 +92,26 @@ public class QuadPrefixTree extends SpatialPrefixTree { this(ctx, ctx.getWorldBounds(), maxLevels); } - public void printInfo() { - NumberFormat nf = NumberFormat.getNumberInstance(); + public void printInfo(PrintStream out) { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.ROOT); nf.setMaximumFractionDigits(5); nf.setMinimumFractionDigits(5); nf.setMinimumIntegerDigits(3); for (int i = 0; i < maxLevels; i++) { - System.out.println(i + "]\t" + nf.format(levelW[i]) + "\t" + nf.format(levelH[i]) + "\t" + + out.println(i + "]\t" + nf.format(levelW[i]) + "\t" + nf.format(levelH[i]) + "\t" + levelS[i] + "\t" + (levelS[i] * levelS[i])); } } @Override public int getLevelForDistance(double dist) { - for (int i = 1; i < maxLevels; i++) { + if (dist == 0)//short circuit + return maxLevels; + for (int i = 0; i < maxLevels - 1; i++) { //note: level[i] is actually a lookup for level i+1 - if(dist > levelW[i] || dist > levelH[i]) { - return i; + if (dist > levelW[i] && dist > levelH[i]) { + return i + 1; } } return maxLevels; @@ -115,7 +120,7 @@ public class QuadPrefixTree extends SpatialPrefixTree { @Override public Node getNode(Point p, int level) { List cells = new ArrayList(1); - build(xmid, ymid, 0, cells, new StringBuilder(), new PointImpl(p.getX(),p.getY()), level); + build(xmid, ymid, 0, cells, new StringBuilder(), ctx.makePoint(p.getX(), p.getY()), level); return cells.get(0);//note cells could be longer if p on edge } @@ -176,21 +181,21 @@ public class QuadPrefixTree extends SpatialPrefixTree { double h = levelH[level] / 2; int strlen = str.length(); - Rectangle rectangle = ctx.makeRect(cx - w, cx + w, cy - h, cy + h); - SpatialRelation v = shape.relate(rectangle, ctx); + Rectangle rectangle = ctx.makeRectangle(cx - w, cx + w, cy - h, cy + h); + SpatialRelation v = shape.relate(rectangle); if (SpatialRelation.CONTAINS == v) { str.append(c); //str.append(SpatialPrefixGrid.COVER); - matches.add(new QuadCell(str.toString(),v.transpose())); + matches.add(new QuadCell(str.toString(), v.transpose())); } else if (SpatialRelation.DISJOINT == v) { // nothing } else { // SpatialRelation.WITHIN, SpatialRelation.INTERSECTS str.append(c); - int nextLevel = level+1; + int nextLevel = level + 1; if (nextLevel >= maxLevel) { //str.append(SpatialPrefixGrid.INTERSECTS); - matches.add(new QuadCell(str.toString(),v.transpose())); + matches.add(new QuadCell(str.toString(), v.transpose())); } else { build(cx, cy, nextLevel, matches, str, shape, maxLevel); } @@ -222,10 +227,10 @@ public class QuadPrefixTree extends SpatialPrefixTree { @Override public Collection getSubCells() { List cells = new ArrayList(4); - cells.add(new QuadCell(getTokenString()+"A")); - cells.add(new QuadCell(getTokenString()+"B")); - cells.add(new QuadCell(getTokenString()+"C")); - cells.add(new QuadCell(getTokenString()+"D")); + cells.add(new QuadCell(getTokenString() + "A")); + cells.add(new QuadCell(getTokenString() + "B")); + cells.add(new QuadCell(getTokenString() + "C")); + cells.add(new QuadCell(getTokenString() + "D")); return cells; } @@ -236,7 +241,7 @@ public class QuadPrefixTree extends SpatialPrefixTree { @Override public Node getSubCell(Point p) { - return QuadPrefixTree.this.getNode(p,getLevel()+1);//not performant! + return QuadPrefixTree.this.getNode(p, getLevel() + 1);//not performant! } private Shape shape;//cache @@ -262,8 +267,7 @@ public class QuadPrefixTree extends SpatialPrefixTree { ymin += levelH[i]; } else if ('C' == c || 'c' == c) { // nothing really - } - else if('D' == c || 'd' == c) { + } else if ('D' == c || 'd' == c) { xmin += levelW[i]; } else { throw new RuntimeException("unexpected char: " + c); @@ -272,13 +276,13 @@ public class QuadPrefixTree extends SpatialPrefixTree { int len = token.length(); double width, height; if (len > 0) { - width = levelW[len-1]; - height = levelH[len-1]; + width = levelW[len - 1]; + height = levelH[len - 1]; } else { width = gridW; height = gridH; } - return ctx.makeRect(xmin, xmin + width, ymin, ymin + height); + return ctx.makeRectangle(xmin, xmin + width, ymin, ymin + height); } }//QuadCell } diff --git a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/SpatialPrefixTree.java b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/SpatialPrefixTree.java index d72718a1c57..0b345e8d9c5 100644 --- a/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/SpatialPrefixTree.java +++ b/src/main/java/org/elasticsearch/common/lucene/spatial/prefix/tree/SpatialPrefixTree.java @@ -28,10 +28,12 @@ import java.util.Collections; import java.util.List; /** - * A Spatial Prefix Tree, or Trie, which decomposes shapes into prefixed strings at variable lengths corresponding to + * A spatial Prefix Tree, or Trie, which decomposes shapes into prefixed strings at variable lengths corresponding to * variable precision. Each string corresponds to a spatial region. - * + *

* Implementations of this class should be thread-safe and immutable once initialized. + * + * @lucene.experimental */ public abstract class SpatialPrefixTree { @@ -61,34 +63,14 @@ public abstract class SpatialPrefixTree { } /** - * See {@link com.spatial4j.core.query.SpatialArgs#getDistPrecision()}. - * A grid level looked up via {@link #getLevelForDistance(double)} is returned. - * - * @param shape - * @param precision 0-0.5 - * @return 1-maxLevels - */ - public int getMaxLevelForPrecision(Shape shape, double precision) { - if (precision < 0 || precision > 0.5) { - throw new IllegalArgumentException("Precision " + precision + " must be between [0-0.5]"); - } - if (precision == 0 || shape instanceof Point) { - return maxLevels; - } - double bboxArea = shape.getBoundingBox().getArea(); - if (bboxArea == 0) { - return maxLevels; - } - double avgSideLenFromCenter = Math.sqrt(bboxArea) / 2; - return getLevelForDistance(avgSideLenFromCenter * precision); - } - - /** - * Returns the level of the smallest grid size with a side length that is greater or equal to the provided - * distance. + * Returns the level of the largest grid in which its longest side is less + * than or equal to the provided distance (in degrees). Consequently {@code + * dist} acts as an error epsilon declaring the amount of detail needed in the + * grid, such that you can get a grid with just the right amount of + * precision. * * @param dist >= 0 - * @return level [1-maxLevels] + * @return level [1 to maxLevels] */ public abstract int getLevelForDistance(double dist); diff --git a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java index 7721a329557..a9782c6c2d3 100644 --- a/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java +++ b/src/main/java/org/elasticsearch/index/mapper/geo/GeoShapeFieldMapper.java @@ -1,14 +1,12 @@ package org.elasticsearch.index.mapper.geo; -import com.spatial4j.core.context.SpatialContext; -import com.spatial4j.core.context.jts.JtsSpatialContext; -import com.spatial4j.core.distance.DistanceUnits; import org.apache.lucene.document.Field; import org.apache.lucene.document.Fieldable; import org.apache.lucene.index.FieldInfo; import org.elasticsearch.ElasticSearchIllegalArgumentException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.geo.GeoJSONShapeParser; +import org.elasticsearch.common.geo.GeoShapeConstants; import org.elasticsearch.common.lucene.spatial.SpatialStrategy; import org.elasticsearch.common.lucene.spatial.prefix.TermQueryPrefixTreeStrategy; import org.elasticsearch.common.lucene.spatial.prefix.tree.GeohashPrefixTree; @@ -44,9 +42,6 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { public static final String CONTENT_TYPE = "geo_shape"; - // TODO: Unsure if the units actually matter since we dont do distance calculations - public static final SpatialContext SPATIAL_CONTEXT = new JtsSpatialContext(DistanceUnits.KILOMETERS); - public static class Names { public static final String TREE = "tree"; public static final String TREE_LEVELS = "tree_levels"; @@ -93,10 +88,10 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper { public GeoShapeFieldMapper build(BuilderContext context) { if (tree.equals(Names.GEOHASH)) { int levels = treeLevels != 0 ? treeLevels : Defaults.GEOHASH_LEVELS; - prefixTree = new GeohashPrefixTree(SPATIAL_CONTEXT, levels); + prefixTree = new GeohashPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, levels); } else if (tree.equals(Names.QUADTREE)) { int levels = treeLevels != 0 ? treeLevels : Defaults.QUADTREE_LEVELS; - prefixTree = new QuadPrefixTree(SPATIAL_CONTEXT, levels); + prefixTree = new QuadPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, levels); } else { throw new ElasticSearchIllegalArgumentException("Unknown prefix tree type [" + tree + "]"); } diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java index 83957d1c0b0..99fa03657f1 100644 --- a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java +++ b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeParserTests.java @@ -5,6 +5,7 @@ import com.spatial4j.core.shape.jts.JtsGeometry; import com.spatial4j.core.shape.jts.JtsPoint; import com.vividsolutions.jts.geom.*; import org.elasticsearch.common.geo.GeoJSONShapeParser; +import org.elasticsearch.common.geo.GeoShapeConstants; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; @@ -30,7 +31,7 @@ public class GeoJSONShapeParserTests { .endObject().string(); Point expected = GEOMETRY_FACTORY.createPoint(new Coordinate(100.0, 0.0)); - assertGeometryEquals(new JtsPoint(expected), pointGeoJson); + assertGeometryEquals(new JtsPoint(expected, GeoShapeConstants.SPATIAL_CONTEXT), pointGeoJson); } @Test @@ -48,7 +49,7 @@ public class GeoJSONShapeParserTests { LineString expected = GEOMETRY_FACTORY.createLineString( lineCoordinates.toArray(new Coordinate[lineCoordinates.size()])); - assertGeometryEquals(new JtsGeometry(expected), lineGeoJson); + assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), lineGeoJson); } @Test @@ -75,7 +76,7 @@ public class GeoJSONShapeParserTests { LinearRing shell = GEOMETRY_FACTORY.createLinearRing( shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); Polygon expected = GEOMETRY_FACTORY.createPolygon(shell, null); - assertGeometryEquals(new JtsGeometry(expected), polygonGeoJson); + assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), polygonGeoJson); } @Test @@ -119,7 +120,7 @@ public class GeoJSONShapeParserTests { holes[0] = GEOMETRY_FACTORY.createLinearRing( holeCoordinates.toArray(new Coordinate[holeCoordinates.size()])); Polygon expected = GEOMETRY_FACTORY.createPolygon(shell, holes); - assertGeometryEquals(new JtsGeometry(expected), polygonGeoJson); + assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), polygonGeoJson); } @Test @@ -137,7 +138,7 @@ public class GeoJSONShapeParserTests { MultiPoint expected = GEOMETRY_FACTORY.createMultiPoint( multiPointCoordinates.toArray(new Coordinate[multiPointCoordinates.size()])); - assertGeometryEquals(new JtsGeometry(expected), multiPointGeoJson); + assertGeometryEquals(new JtsGeometry(expected, GeoShapeConstants.SPATIAL_CONTEXT, false), multiPointGeoJson); } private void assertGeometryEquals(Shape expected, String geoJson) throws IOException { diff --git a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java index 007076566bd..344ba0cb88d 100644 --- a/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java +++ b/src/test/java/org/elasticsearch/test/unit/common/geo/GeoJSONShapeSerializerTests.java @@ -5,6 +5,7 @@ import com.spatial4j.core.shape.jts.JtsGeometry; import com.spatial4j.core.shape.jts.JtsPoint; import com.vividsolutions.jts.geom.*; import org.elasticsearch.common.geo.GeoJSONShapeSerializer; +import org.elasticsearch.common.geo.GeoShapeConstants; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.testng.annotations.Test; @@ -29,7 +30,7 @@ public class GeoJSONShapeSerializerTests { .endObject(); Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(100.0, 0.0)); - assertSerializationEquals(expected, new JtsPoint(point)); + assertSerializationEquals(expected, new JtsPoint(point, GeoShapeConstants.SPATIAL_CONTEXT)); } @Test @@ -48,7 +49,7 @@ public class GeoJSONShapeSerializerTests { LineString lineString = GEOMETRY_FACTORY.createLineString( lineCoordinates.toArray(new Coordinate[lineCoordinates.size()])); - assertSerializationEquals(expected, new JtsGeometry(lineString)); + assertSerializationEquals(expected, new JtsGeometry(lineString, GeoShapeConstants.SPATIAL_CONTEXT, false)); } @Test @@ -76,7 +77,7 @@ public class GeoJSONShapeSerializerTests { shellCoordinates.toArray(new Coordinate[shellCoordinates.size()])); Polygon polygon = GEOMETRY_FACTORY.createPolygon(shell, null); - assertSerializationEquals(expected, new JtsGeometry(polygon)); + assertSerializationEquals(expected, new JtsGeometry(polygon, GeoShapeConstants.SPATIAL_CONTEXT, false)); } @Test @@ -121,7 +122,7 @@ public class GeoJSONShapeSerializerTests { holeCoordinates.toArray(new Coordinate[holeCoordinates.size()])); Polygon polygon = GEOMETRY_FACTORY.createPolygon(shell, holes); - assertSerializationEquals(expected, new JtsGeometry(polygon)); + assertSerializationEquals(expected, new JtsGeometry(polygon, GeoShapeConstants.SPATIAL_CONTEXT, false)); } @Test @@ -140,7 +141,7 @@ public class GeoJSONShapeSerializerTests { MultiPoint multiPoint = GEOMETRY_FACTORY.createMultiPoint( multiPointCoordinates.toArray(new Coordinate[multiPointCoordinates.size()])); - assertSerializationEquals(expected, new JtsGeometry(multiPoint)); + assertSerializationEquals(expected, new JtsGeometry(multiPoint, GeoShapeConstants.SPATIAL_CONTEXT, false)); } private void assertSerializationEquals(XContentBuilder expected, Shape shape) throws IOException { diff --git a/src/test/java/org/elasticsearch/test/unit/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategyTests.java b/src/test/java/org/elasticsearch/test/unit/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategyTests.java index 34e0ade9d5a..6ad27e6c7a5 100644 --- a/src/test/java/org/elasticsearch/test/unit/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategyTests.java +++ b/src/test/java/org/elasticsearch/test/unit/common/lucene/spatial/prefix/TermQueryPrefixTreeStrategyTests.java @@ -1,8 +1,5 @@ package org.elasticsearch.test.unit.common.lucene.spatial.prefix; -import com.spatial4j.core.context.SpatialContext; -import com.spatial4j.core.context.jts.JtsSpatialContext; -import com.spatial4j.core.distance.DistanceUnits; import com.spatial4j.core.shape.Rectangle; import com.spatial4j.core.shape.Shape; import org.apache.lucene.analysis.KeywordAnalyzer; @@ -16,6 +13,7 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.store.RAMDirectory; import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.Version; +import org.elasticsearch.common.geo.GeoShapeConstants; import org.elasticsearch.common.lucene.spatial.prefix.TermQueryPrefixTreeStrategy; import org.elasticsearch.common.lucene.spatial.prefix.tree.GeohashPrefixTree; import org.elasticsearch.common.lucene.spatial.prefix.tree.QuadPrefixTree; @@ -37,13 +35,12 @@ import static org.testng.Assert.assertTrue; */ public class TermQueryPrefixTreeStrategyTests { - private static final SpatialContext SPATIAL_CONTEXT = new JtsSpatialContext(DistanceUnits.KILOMETERS); // TODO: Randomize the implementation choice private static final SpatialPrefixTree QUAD_PREFIX_TREE = - new QuadPrefixTree(SPATIAL_CONTEXT, QuadPrefixTree.DEFAULT_MAX_LEVELS); + new QuadPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, QuadPrefixTree.DEFAULT_MAX_LEVELS); private static final SpatialPrefixTree GEOHASH_PREFIX_TREE - = new GeohashPrefixTree(SPATIAL_CONTEXT, GeohashPrefixTree.getMaxLevelsPossible()); + = new GeohashPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, GeohashPrefixTree.getMaxLevelsPossible()); private static final TermQueryPrefixTreeStrategy STRATEGY = new TermQueryPrefixTreeStrategy(new FieldMapper.Names("shape"), GEOHASH_PREFIX_TREE, 0.025);