You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by nk...@apache.org on 2016/02/05 18:11:12 UTC
[31/34] lucene-solr git commit: LUCENE-7015: refactors lucene-spatial
module to a new lucene-spatial-extras module,
and refactors sandbox GeoPointField and queries to lucene-spatial module
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
----------------------------------------------------------------------
diff --git a/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java b/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
deleted file mode 100644
index 5650614..0000000
--- a/lucene/sandbox/src/test/org/apache/lucene/util/TestGeoUtils.java
+++ /dev/null
@@ -1,545 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF 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.apache.lucene.util;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import org.junit.BeforeClass;
-
-import com.carrotsearch.randomizedtesting.generators.RandomInts;
-
-import static org.apache.lucene.util.GeoDistanceUtils.DISTANCE_PCT_ERR;
-
-/**
- * Tests class for methods in GeoUtils
- *
- * @lucene.experimental
- */
-public class TestGeoUtils extends LuceneTestCase {
-
- private static final double LON_SCALE = (0x1L<<GeoUtils.BITS)/360.0D;
- private static final double LAT_SCALE = (0x1L<<GeoUtils.BITS)/180.0D;
-
- // Global bounding box we will "cover" in the random test; we have to make this "smallish" else the queries take very long:
- private static double originLat;
- private static double originLon;
- // private static double range;
- private static double lonRange;
- private static double latRange;
-
- @BeforeClass
- public static void beforeClass() throws Exception {
- // Between 1.0 and 3.0:
- lonRange = 2 * (random().nextDouble() + 0.5);
- latRange = 2 * (random().nextDouble() + 0.5);
-
- originLon = GeoUtils.MIN_LON_INCL + lonRange + (GeoUtils.MAX_LON_INCL - GeoUtils.MIN_LON_INCL - 2 * lonRange) * random().nextDouble();
- originLon = GeoUtils.normalizeLon(originLon);
- originLat = GeoUtils.MIN_LAT_INCL + latRange + (GeoUtils.MAX_LAT_INCL - GeoUtils.MIN_LAT_INCL - 2 * latRange) * random().nextDouble();
- originLat = GeoUtils.normalizeLat(originLat);
-
- if (VERBOSE) {
- System.out.println("TEST: originLon=" + originLon + " lonRange= " + lonRange + " originLat=" + originLat + " latRange=" + latRange);
- }
- }
-
- public void testGeoHash() {
- int numPoints = atLeast(100);
- String randomGeoHashString;
- String mortonGeoHash;
- long mortonLongFromGHLong, geoHashLong, mortonLongFromGHString;
- int randomLevel;
- for (int i = 0; i < numPoints; ++i) {
- // random point
- double lat = randomLat(false);
- double lon = randomLon(false);
-
- // compute geohash straight from lat/lon and from morton encoded value to ensure they're the same
- randomGeoHashString = GeoHashUtils.stringEncode(lon, lat, randomLevel = random().nextInt(12 - 1) + 1);
- mortonGeoHash = GeoHashUtils.stringEncodeFromMortonLong(GeoUtils.mortonHash(lon, lat), randomLevel);
- assertEquals(randomGeoHashString, mortonGeoHash);
-
- // v&v conversion from lat/lon or geohashstring to geohash long and back to geohash string
- geoHashLong = (random().nextBoolean()) ? GeoHashUtils.longEncode(lon, lat, randomLevel) : GeoHashUtils.longEncode(randomGeoHashString);
- assertEquals(randomGeoHashString, GeoHashUtils.stringEncode(geoHashLong));
-
- // v&v conversion from geohash long to morton long
- mortonLongFromGHString = GeoHashUtils.mortonEncode(randomGeoHashString);
- mortonLongFromGHLong = GeoHashUtils.mortonEncode(geoHashLong);
- assertEquals(mortonLongFromGHLong, mortonLongFromGHString);
-
- // v&v lat/lon from geohash string and geohash long
- assertEquals(GeoUtils.mortonUnhashLat(mortonLongFromGHString), GeoUtils.mortonUnhashLat(mortonLongFromGHLong), 0);
- assertEquals(GeoUtils.mortonUnhashLon(mortonLongFromGHString), GeoUtils.mortonUnhashLon(mortonLongFromGHLong), 0);
- }
- }
-
- /**
- * Pass condition: lat=42.6, lng=-5.6 should be encoded as "ezs42e44yx96",
- * lat=57.64911 lng=10.40744 should be encoded as "u4pruydqqvj8"
- */
- public void testEncode() {
- String hash = GeoHashUtils.stringEncode(-5.6, 42.6, 12);
- assertEquals("ezs42e44yx96", hash);
-
- hash = GeoHashUtils.stringEncode(10.40744, 57.64911, 12);
- assertEquals("u4pruydqqvj8", hash);
- }
-
- /**
- * Pass condition: lat=52.3738007, lng=4.8909347 should be encoded and then
- * decoded within 0.00001 of the original value
- */
- public void testDecodePreciseLongitudeLatitude() {
- final String geohash = GeoHashUtils.stringEncode(4.8909347, 52.3738007);
- final long hash = GeoHashUtils.mortonEncode(geohash);
-
- assertEquals(52.3738007, GeoUtils.mortonUnhashLat(hash), 0.00001D);
- assertEquals(4.8909347, GeoUtils.mortonUnhashLon(hash), 0.00001D);
- }
-
- /**
- * Pass condition: lat=84.6, lng=10.5 should be encoded and then decoded
- * within 0.00001 of the original value
- */
- public void testDecodeImpreciseLongitudeLatitude() {
- final String geohash = GeoHashUtils.stringEncode(10.5, 84.6);
-
- final long hash = GeoHashUtils.mortonEncode(geohash);
-
- assertEquals(84.6, GeoUtils.mortonUnhashLat(hash), 0.00001D);
- assertEquals(10.5, GeoUtils.mortonUnhashLon(hash), 0.00001D);
- }
-
- public void testDecodeEncode() {
- final String geoHash = "u173zq37x014";
- assertEquals(geoHash, GeoHashUtils.stringEncode(4.8909347, 52.3738007));
- final long mortonHash = GeoHashUtils.mortonEncode(geoHash);
- final double lon = GeoUtils.mortonUnhashLon(mortonHash);
- final double lat = GeoUtils.mortonUnhashLat(mortonHash);
- assertEquals(52.37380061d, GeoUtils.mortonUnhashLat(mortonHash), 0.000001d);
- assertEquals(4.8909343d, GeoUtils.mortonUnhashLon(mortonHash), 0.000001d);
-
- assertEquals(geoHash, GeoHashUtils.stringEncode(lon, lat));
- }
-
- public void testNeighbors() {
- String geohash = "gcpv";
- List<String> expectedNeighbors = new ArrayList<>();
- expectedNeighbors.add("gcpw");
- expectedNeighbors.add("gcpy");
- expectedNeighbors.add("u10n");
- expectedNeighbors.add("gcpt");
- expectedNeighbors.add("u10j");
- expectedNeighbors.add("gcps");
- expectedNeighbors.add("gcpu");
- expectedNeighbors.add("u10h");
- Collection<? super String> neighbors = new ArrayList<>();
- GeoHashUtils.addNeighbors(geohash, neighbors );
- assertEquals(expectedNeighbors, neighbors);
-
- // Border odd geohash
- geohash = "u09x";
- expectedNeighbors = new ArrayList<>();
- expectedNeighbors.add("u0c2");
- expectedNeighbors.add("u0c8");
- expectedNeighbors.add("u0cb");
- expectedNeighbors.add("u09r");
- expectedNeighbors.add("u09z");
- expectedNeighbors.add("u09q");
- expectedNeighbors.add("u09w");
- expectedNeighbors.add("u09y");
- neighbors = new ArrayList<>();
- GeoHashUtils.addNeighbors(geohash, neighbors);
- assertEquals(expectedNeighbors, neighbors);
-
- // Border even geohash
- geohash = "u09tv";
- expectedNeighbors = new ArrayList<>();
- expectedNeighbors.add("u09wh");
- expectedNeighbors.add("u09wj");
- expectedNeighbors.add("u09wn");
- expectedNeighbors.add("u09tu");
- expectedNeighbors.add("u09ty");
- expectedNeighbors.add("u09ts");
- expectedNeighbors.add("u09tt");
- expectedNeighbors.add("u09tw");
- neighbors = new ArrayList<>();
- GeoHashUtils.addNeighbors(geohash, neighbors );
- assertEquals(expectedNeighbors, neighbors);
-
- // Border even and odd geohash
- geohash = "ezzzz";
- expectedNeighbors = new ArrayList<>();
- expectedNeighbors.add("gbpbn");
- expectedNeighbors.add("gbpbp");
- expectedNeighbors.add("u0000");
- expectedNeighbors.add("ezzzy");
- expectedNeighbors.add("spbpb");
- expectedNeighbors.add("ezzzw");
- expectedNeighbors.add("ezzzx");
- expectedNeighbors.add("spbp8");
- neighbors = new ArrayList<>();
- GeoHashUtils.addNeighbors(geohash, neighbors );
- assertEquals(expectedNeighbors, neighbors);
- }
-
- public void testClosestPointOnBBox() {
- double[] result = new double[2];
- GeoDistanceUtils.closestPointOnBBox(20, 30, 40, 50, 70, 70, result);
- assertEquals(40.0, result[0], 0.0);
- assertEquals(50.0, result[1], 0.0);
-
- GeoDistanceUtils.closestPointOnBBox(-20, -20, 0, 0, 70, 70, result);
- assertEquals(0.0, result[0], 0.0);
- assertEquals(0.0, result[1], 0.0);
- }
-
- private static class Cell {
- static int nextCellID;
-
- final Cell parent;
- final int cellID;
- final double minLon, maxLon;
- final double minLat, maxLat;
- final int splitCount;
-
- public Cell(Cell parent,
- double minLon, double minLat,
- double maxLon, double maxLat,
- int splitCount) {
- assert maxLon >= minLon;
- assert maxLat >= minLat;
- this.parent = parent;
- this.minLon = minLon;
- this.minLat = minLat;
- this.maxLon = maxLon;
- this.maxLat = maxLat;
- this.cellID = nextCellID++;
- this.splitCount = splitCount;
- }
-
- /** Returns true if the quantized point lies within this cell, inclusive on all bounds. */
- public boolean contains(double lon, double lat) {
- return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat;
- }
-
- @Override
- public String toString() {
- return "cell=" + cellID + (parent == null ? "" : " parentCellID=" + parent.cellID) + " lon: " + minLon + " TO " + maxLon + ", lat: " + minLat + " TO " + maxLat + ", splits: " + splitCount;
- }
- }
-
- public long scaleLon(final double val) {
- return (long) ((val-GeoUtils.MIN_LON_INCL) * LON_SCALE);
- }
-
- public long scaleLat(final double val) {
- return (long) ((val-GeoUtils.MIN_LAT_INCL) * LAT_SCALE);
- }
-
- public double unscaleLon(final long val) {
- return (val / LON_SCALE) + GeoUtils.MIN_LON_INCL;
- }
-
- public double unscaleLat(final long val) {
- return (val / LAT_SCALE) + GeoUtils.MIN_LAT_INCL;
- }
-
- public double randomLat(boolean small) {
- double result;
- if (small) {
- result = GeoUtils.normalizeLat(originLat + latRange * (random().nextDouble() - 0.5));
- } else {
- result = -90 + 180.0 * random().nextDouble();
- }
- return result;
- }
-
- public double randomLon(boolean small) {
- double result;
- if (small) {
- result = GeoUtils.normalizeLon(originLon + lonRange * (random().nextDouble() - 0.5));
- } else {
- result = -180 + 360.0 * random().nextDouble();
- }
- return result;
- }
-
- private void findMatches(Set<Integer> hits, PrintWriter log, Cell root,
- double centerLon, double centerLat, double radiusMeters,
- double[] docLons, double[] docLats) {
-
- if (VERBOSE) {
- log.println(" root cell: " + root);
- }
-
- List<Cell> queue = new ArrayList<>();
- queue.add(root);
-
- int recurseDepth = RandomInts.randomIntBetween(random(), 5, 15);
-
- while (queue.size() > 0) {
- Cell cell = queue.get(queue.size()-1);
- queue.remove(queue.size()-1);
- if (VERBOSE) {
- log.println(" cycle: " + cell + " queue.size()=" + queue.size());
- }
-
- if (random().nextInt(10) == 7 || cell.splitCount > recurseDepth) {
- if (VERBOSE) {
- log.println(" leaf");
- }
- // Leaf cell: brute force check all docs that fall within this cell:
- for(int docID=0;docID<docLons.length;docID++) {
- if (cell.contains(docLons[docID], docLats[docID])) {
- double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, docLats[docID], docLons[docID]);
- if (distanceMeters <= radiusMeters) {
- if (VERBOSE) {
- log.println(" check doc=" + docID + ": match!");
- }
- hits.add(docID);
- } else {
- if (VERBOSE) {
- log.println(" check doc=" + docID + ": no match");
- }
- }
- }
- }
- } else {
-
- if (GeoRelationUtils.rectWithinCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat, centerLon, centerLat, radiusMeters)) {
- // Query circle fully contains this cell, just addAll:
- if (VERBOSE) {
- log.println(" circle fully contains cell: now addAll");
- }
- for(int docID=0;docID<docLons.length;docID++) {
- if (cell.contains(docLons[docID], docLats[docID])) {
- if (VERBOSE) {
- log.println(" addAll doc=" + docID);
- }
- hits.add(docID);
- }
- }
- continue;
- } else if (GeoRelationUtils.rectWithin(root.minLon, root.minLat, root.maxLon, root.maxLat,
- cell.minLon, cell.minLat, cell.maxLon, cell.maxLat)) {
- // Fall through below to "recurse"
- if (VERBOSE) {
- log.println(" cell fully contains circle: keep splitting");
- }
- } else if (GeoRelationUtils.rectCrossesCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat,
- centerLon, centerLat, radiusMeters)) {
- // Fall through below to "recurse"
- if (VERBOSE) {
- log.println(" cell overlaps circle: keep splitting");
- }
- } else {
- if (VERBOSE) {
- log.println(" no overlap: drop this cell");
- for(int docID=0;docID<docLons.length;docID++) {
- if (cell.contains(docLons[docID], docLats[docID])) {
- if (VERBOSE) {
- log.println(" skip doc=" + docID);
- }
- }
- }
- }
- continue;
- }
-
- // Randomly split:
- if (random().nextBoolean()) {
-
- // Split on lon:
- double splitValue = cell.minLon + (cell.maxLon - cell.minLon) * random().nextDouble();
- if (VERBOSE) {
- log.println(" now split on lon=" + splitValue);
- }
- Cell cell1 = new Cell(cell,
- cell.minLon, cell.minLat,
- splitValue, cell.maxLat,
- cell.splitCount+1);
- Cell cell2 = new Cell(cell,
- splitValue, cell.minLat,
- cell.maxLon, cell.maxLat,
- cell.splitCount+1);
- if (VERBOSE) {
- log.println(" split cell1: " + cell1);
- log.println(" split cell2: " + cell2);
- }
- queue.add(cell1);
- queue.add(cell2);
- } else {
-
- // Split on lat:
- double splitValue = cell.minLat + (cell.maxLat - cell.minLat) * random().nextDouble();
- if (VERBOSE) {
- log.println(" now split on lat=" + splitValue);
- }
- Cell cell1 = new Cell(cell,
- cell.minLon, cell.minLat,
- cell.maxLon, splitValue,
- cell.splitCount+1);
- Cell cell2 = new Cell(cell,
- cell.minLon, splitValue,
- cell.maxLon, cell.maxLat,
- cell.splitCount+1);
- if (VERBOSE) {
- log.println(" split cells:\n " + cell1 + "\n " + cell2);
- }
- queue.add(cell1);
- queue.add(cell2);
- }
- }
- }
- }
-
- /** Tests consistency of GeoUtils.rectWithinCircle, .rectCrossesCircle, .rectWithin and SloppyMath.haversine distance check */
- public void testGeoRelations() throws Exception {
-
- int numDocs = atLeast(1000);
-
- boolean useSmallRanges = random().nextBoolean();
-
- if (VERBOSE) {
- System.out.println("TEST: " + numDocs + " docs useSmallRanges=" + useSmallRanges);
- }
-
- double[] docLons = new double[numDocs];
- double[] docLats = new double[numDocs];
- for(int docID=0;docID<numDocs;docID++) {
- docLons[docID] = randomLon(useSmallRanges);
- docLats[docID] = randomLat(useSmallRanges);
- if (VERBOSE) {
- System.out.println(" doc=" + docID + ": lon=" + docLons[docID] + " lat=" + docLats[docID]);
- }
- }
-
- int iters = atLeast(10);
-
- iters = atLeast(50);
-
- for(int iter=0;iter<iters;iter++) {
-
- Cell.nextCellID = 0;
-
- double centerLon = randomLon(useSmallRanges);
- double centerLat = randomLat(useSmallRanges);
-
- // So the circle covers at most 50% of the earth's surface:
-
- double radiusMeters;
-
- // TODO: large exotic rectangles created by BKD may be inaccurate up to 2 times DISTANCE_PCT_ERR.
- // restricting size until LUCENE-6994 can be addressed
- if (true || useSmallRanges) {
- // Approx 3 degrees lon at the equator:
- radiusMeters = random().nextDouble() * 333000;
- } else {
- radiusMeters = random().nextDouble() * GeoProjectionUtils.SEMIMAJOR_AXIS * Math.PI / 2.0;
- }
-
- StringWriter sw = new StringWriter();
- PrintWriter log = new PrintWriter(sw, true);
-
- if (VERBOSE) {
- log.println("\nTEST: iter=" + iter + " radiusMeters=" + radiusMeters + " centerLon=" + centerLon + " centerLat=" + centerLat);
- }
-
- GeoRect bbox = GeoUtils.circleToBBox(centerLon, centerLat, radiusMeters);
-
- Set<Integer> hits = new HashSet<>();
-
- if (bbox.maxLon < bbox.minLon) {
- // Crosses dateline
- log.println(" circle crosses dateline; first left query");
- double unwrappedLon = centerLon;
- if (unwrappedLon > bbox.maxLon) {
- // unwrap left
- unwrappedLon += -360.0D;
- }
- findMatches(hits, log,
- new Cell(null,
- -180, bbox.minLat,
- bbox.maxLon, bbox.maxLat,
- 0),
- unwrappedLon, centerLat, radiusMeters, docLons, docLats);
- log.println(" circle crosses dateline; now right query");
- if (unwrappedLon < bbox.maxLon) {
- // unwrap right
- unwrappedLon += 360.0D;
- }
- findMatches(hits, log,
- new Cell(null,
- bbox.minLon, bbox.minLat,
- 180, bbox.maxLat,
- 0),
- unwrappedLon, centerLat, radiusMeters, docLons, docLats);
- } else {
- // Start with the root cell that fully contains the shape:
- findMatches(hits, log,
- new Cell(null,
- bbox.minLon, bbox.minLat,
- bbox.maxLon, bbox.maxLat,
- 0),
- centerLon, centerLat, radiusMeters,
- docLons, docLats);
- }
-
- if (VERBOSE) {
- log.println(" " + hits.size() + " hits");
- }
-
- int failCount = 0;
-
- // Done matching, now verify:
- for(int docID=0;docID<numDocs;docID++) {
- double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, docLats[docID], docLons[docID]);
- final Boolean expected;
- final double percentError = Math.abs(distanceMeters - radiusMeters) / distanceMeters;
- if (percentError <= DISTANCE_PCT_ERR) {
- expected = null;
- } else {
- expected = distanceMeters <= radiusMeters;
- }
-
- boolean actual = hits.contains(docID);
- if (expected != null && actual != expected) {
- if (actual) {
- log.println("doc=" + docID + " matched but should not with distance error " + percentError + " on iteration " + iter);
- } else {
- log.println("doc=" + docID + " did not match but should with distance error " + percentError + " on iteration " + iter);
- }
- log.println(" lon=" + docLons[docID] + " lat=" + docLats[docID] + " distanceMeters=" + distanceMeters + " vs radiusMeters=" + radiusMeters);
- failCount++;
- }
- }
-
- if (failCount != 0) {
- System.out.print(sw.toString());
- fail(failCount + " incorrect hits (see above)");
- }
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/spatial-extras/build.xml
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/build.xml b/lucene/spatial-extras/build.xml
new file mode 100644
index 0000000..e1a1365
--- /dev/null
+++ b/lucene/spatial-extras/build.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF 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.
+-->
+<project name="spatial-extras" default="default">
+ <description>
+ Geospatial search
+ </description>
+
+ <import file="../module-build.xml"/>
+
+ <path id="spatialextrasjar">
+ <fileset dir="lib"/>
+ </path>
+
+ <path id="classpath">
+ <path refid="base.classpath"/>
+ <path refid="spatialextrasjar"/>
+ <pathelement path="${queries.jar}" />
+ <pathelement path="${misc.jar}" />
+ <pathelement path="${spatial3d.jar}" />
+ </path>
+
+ <path id="test.classpath">
+ <path refid="test.base.classpath" />
+ <path refid="spatialextrasjar"/>
+ <pathelement path="src/test-files" />
+ </path>
+
+ <target name="compile-core" depends="jar-queries,jar-misc,jar-spatial3d,common.compile-core" />
+
+ <target name="javadocs" depends="javadocs-queries,javadocs-misc,javadocs-spatial3d,compile-core,check-javadocs-uptodate"
+ unless="javadocs-uptodate-${name}">
+ <invoke-module-javadoc>
+ <links>
+ <link href="../queries"/>
+ <link href="../misc"/>
+ <link href="../spatial3d"/>
+ </links>
+ </invoke-module-javadoc>
+ </target>
+</project>
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/spatial-extras/ivy.xml
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/ivy.xml b/lucene/spatial-extras/ivy.xml
new file mode 100644
index 0000000..0229a9f
--- /dev/null
+++ b/lucene/spatial-extras/ivy.xml
@@ -0,0 +1,36 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF 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.
+-->
+<ivy-module version="2.0" xmlns:maven="http://ant.apache.org/ivy/maven">
+ <info organisation="org.apache.lucene" module="spatial-extras"/>
+ <configurations defaultconfmapping="compile->master;test->master">
+ <conf name="compile" transitive="false"/>
+ <conf name="test" transitive="false"/>
+ </configurations>
+ <dependencies>
+ <dependency org="com.spatial4j" name="spatial4j" rev="${/com.spatial4j/spatial4j}" conf="compile"/>
+
+ <dependency org="com.spatial4j" name="spatial4j" rev="${/com.spatial4j/spatial4j}" conf="test">
+ <artifact name="spatial4j" type="test" ext="jar" maven:classifier="tests" />
+ </dependency>
+
+ <dependency org="org.slf4j" name="slf4j-api" rev="${/org.slf4j/slf4j-api}" conf="test"/>
+
+ <exclude org="*" ext="*" matcher="regexp" type="${ivy.exclude.types}"/>
+ </dependencies>
+</ivy-module>
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java
new file mode 100644
index 0000000..f433c11
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/SpatialStrategy.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.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.queries.function.ValueSource;
+import org.apache.lucene.queries.function.valuesource.ReciprocalFloatFunction;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.spatial.query.SpatialArgs;
+
+/**
+ * The SpatialStrategy encapsulates an approach to indexing and searching based
+ * on shapes.
+ * <p>
+ * Different implementations will support different features. A strategy should
+ * document these common elements:
+ * <ul>
+ * <li>Can it index more than one shape per field?</li>
+ * <li>What types of shapes can be indexed?</li>
+ * <li>What types of query shapes can be used?</li>
+ * <li>What types of query operations are supported?
+ * This might vary per shape.</li>
+ * <li>Does it use some type of cache? When?
+ * </ul>
+ * If a strategy only supports certain shapes at index or query time, then in
+ * general it will throw an exception if given an incompatible one. It will not
+ * be coerced into compatibility.
+ * <p>
+ * Note that a SpatialStrategy is not involved with the Lucene stored field
+ * values of shapes, which is immaterial to indexing and search.
+ * <p>
+ * Thread-safe.
+ * <p>
+ * This API is marked as experimental, however it is quite stable.
+ *
+ * @lucene.experimental
+ */
+public abstract class SpatialStrategy {
+
+ protected final SpatialContext ctx;
+ private final String fieldName;
+
+ /**
+ * Constructs the spatial strategy with its mandatory arguments.
+ */
+ public SpatialStrategy(SpatialContext ctx, String fieldName) {
+ if (ctx == null)
+ throw new IllegalArgumentException("ctx is required");
+ this.ctx = ctx;
+ if (fieldName == null || fieldName.length() == 0)
+ throw new IllegalArgumentException("fieldName is required");
+ this.fieldName = fieldName;
+ }
+
+ public SpatialContext getSpatialContext() {
+ return ctx;
+ }
+
+ /**
+ * The name of the field or the prefix of them if there are multiple
+ * fields needed internally.
+ * @return Not null.
+ */
+ public String getFieldName() {
+ return fieldName;
+ }
+
+ /**
+ * Returns the IndexableField(s) from the {@code shape} that are to be
+ * added to the {@link org.apache.lucene.document.Document}. These fields
+ * are expected to be marked as indexed and not stored.
+ * <p>
+ * Note: If you want to <i>store</i> the shape as a string for retrieval in
+ * search results, you could add it like this:
+ * <pre>document.add(new StoredField(fieldName,ctx.toString(shape)));</pre>
+ * The particular string representation used doesn't matter to the Strategy
+ * since it doesn't use it.
+ *
+ * @return Not null nor will it have null elements.
+ * @throws UnsupportedOperationException if given a shape incompatible with the strategy
+ */
+ public abstract Field[] createIndexableFields(Shape shape);
+
+ /**
+ * See {@link #makeDistanceValueSource(com.spatial4j.core.shape.Point, double)} called with
+ * a multiplier of 1.0 (i.e. units of degrees).
+ */
+ public ValueSource makeDistanceValueSource(Point queryPoint) {
+ return makeDistanceValueSource(queryPoint, 1.0);
+ }
+
+ /**
+ * Make a ValueSource returning the distance between the center of the
+ * indexed shape and {@code queryPoint}. If there are multiple indexed shapes
+ * then the closest one is chosen. The result is multiplied by {@code multiplier}, which
+ * conveniently is used to get the desired units.
+ */
+ public abstract ValueSource makeDistanceValueSource(Point queryPoint, double multiplier);
+
+ /**
+ * Make a Query based principally on {@link org.apache.lucene.spatial.query.SpatialOperation}
+ * and {@link Shape} from the supplied {@code args}. It should be constant scoring of 1.
+ *
+ * @throws UnsupportedOperationException If the strategy does not support the shape in {@code args}
+ * @throws org.apache.lucene.spatial.query.UnsupportedSpatialOperation If the strategy does not support the {@link
+ * org.apache.lucene.spatial.query.SpatialOperation} in {@code args}.
+ */
+ public abstract Query makeQuery(SpatialArgs args);
+
+ /**
+ * Returns a ValueSource with values ranging from 1 to 0, depending inversely
+ * on the distance from {@link #makeDistanceValueSource(com.spatial4j.core.shape.Point,double)}.
+ * The formula is {@code c/(d + c)} where 'd' is the distance and 'c' is
+ * one tenth the distance to the farthest edge from the center. Thus the
+ * scores will be 1 for indexed points at the center of the query shape and as
+ * low as ~0.1 at its furthest edges.
+ */
+ public final ValueSource makeRecipDistanceValueSource(Shape queryShape) {
+ Rectangle bbox = queryShape.getBoundingBox();
+ double diagonalDist = ctx.getDistCalc().distance(
+ ctx.makePoint(bbox.getMinX(), bbox.getMinY()), bbox.getMaxX(), bbox.getMaxY());
+ double distToEdge = diagonalDist * 0.5;
+ float c = (float)distToEdge * 0.1f;//one tenth
+ return new ReciprocalFloatFunction(makeDistanceValueSource(queryShape.getCenter(), 1.0), 1f, c, c);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()+" field:"+fieldName+" ctx="+ctx;
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java
new file mode 100644
index 0000000..9d0afe1
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxOverlapRatioValueSource.java
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.lucene.spatial.bbox;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.Explanation;
+
+import com.spatial4j.core.shape.Rectangle;
+
+/**
+ * The algorithm is implemented as envelope on envelope (rect on rect) overlays rather than
+ * complex polygon on complex polygon overlays.
+ * <p>
+ * Spatial relevance scoring algorithm:
+ * <DL>
+ * <DT>queryArea</DT> <DD>the area of the input query envelope</DD>
+ * <DT>targetArea</DT> <DD>the area of the target envelope (per Lucene document)</DD>
+ * <DT>intersectionArea</DT> <DD>the area of the intersection between the query and target envelopes</DD>
+ * <DT>queryTargetProportion</DT> <DD>A 0-1 factor that divides the score proportion between query and target.
+ * 0.5 is evenly.</DD>
+ *
+ * <DT>queryRatio</DT> <DD>intersectionArea / queryArea; (see note)</DD>
+ * <DT>targetRatio</DT> <DD>intersectionArea / targetArea; (see note)</DD>
+ * <DT>queryFactor</DT> <DD>queryRatio * queryTargetProportion;</DD>
+ * <DT>targetFactor</DT> <DD>targetRatio * (1 - queryTargetProportion);</DD>
+ * <DT>score</DT> <DD>queryFactor + targetFactor;</DD>
+ * </DL>
+ * Additionally, note that an optional minimum side length {@code minSideLength} may be used whenever an
+ * area is calculated (queryArea, targetArea, intersectionArea). This allows for points or horizontal/vertical lines
+ * to be used as the query shape and in such case the descending order should have smallest boxes up front. Without
+ * this, a point or line query shape typically scores everything with the same value since there is 0 area.
+ * <p>
+ * Note: The actual computation of queryRatio and targetRatio is more complicated so that it considers
+ * points and lines. Lines have the ratio of overlap, and points are either 1.0 or 0.0 depending on whether
+ * it intersects or not.
+ * <p>
+ * Originally based on Geoportal's
+ * <a href="http://geoportal.svn.sourceforge.net/svnroot/geoportal/Geoportal/trunk/src/com/esri/gpt/catalog/lucene/SpatialRankingValueSource.java">
+ * SpatialRankingValueSource</a> but modified quite a bit. GeoPortal's algorithm will yield a score of 0
+ * if either a line or point is compared, and it doesn't output a 0-1 normalized score (it multiplies the factors),
+ * and it doesn't support minSideLength, and it had dateline bugs.
+ *
+ * @lucene.experimental
+ */
+public class BBoxOverlapRatioValueSource extends BBoxSimilarityValueSource {
+
+ private final boolean isGeo;//-180/+180 degrees (not part of identity; attached to parent strategy/field)
+
+ private final Rectangle queryExtent;
+ private final double queryArea;//not part of identity
+
+ private final double minSideLength;
+
+ private final double queryTargetProportion;
+
+ //TODO option to compute geodetic area
+
+ /**
+ *
+ * @param rectValueSource mandatory; source of rectangles
+ * @param isGeo True if ctx.isGeo() and thus dateline issues should be attended to
+ * @param queryExtent mandatory; the query rectangle
+ * @param queryTargetProportion see class javadocs. Between 0 and 1.
+ * @param minSideLength see class javadocs. 0.0 will effectively disable.
+ */
+ public BBoxOverlapRatioValueSource(ValueSource rectValueSource, boolean isGeo, Rectangle queryExtent,
+ double queryTargetProportion, double minSideLength) {
+ super(rectValueSource);
+ this.isGeo = isGeo;
+ this.minSideLength = minSideLength;
+ this.queryExtent = queryExtent;
+ this.queryArea = calcArea(queryExtent.getWidth(), queryExtent.getHeight());
+ assert queryArea >= 0;
+ this.queryTargetProportion = queryTargetProportion;
+ if (queryTargetProportion < 0 || queryTargetProportion > 1.0)
+ throw new IllegalArgumentException("queryTargetProportion must be >= 0 and <= 1");
+ }
+
+ /** Construct with 75% weighting towards target (roughly GeoPortal's default), geo degrees assumed, no
+ * minimum side length. */
+ public BBoxOverlapRatioValueSource(ValueSource rectValueSource, Rectangle queryExtent) {
+ this(rectValueSource, true, queryExtent, 0.25, 0.0);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!super.equals(o)) return false;
+
+ BBoxOverlapRatioValueSource that = (BBoxOverlapRatioValueSource) o;
+
+ if (Double.compare(that.minSideLength, minSideLength) != 0) return false;
+ if (Double.compare(that.queryTargetProportion, queryTargetProportion) != 0) return false;
+ if (!queryExtent.equals(that.queryExtent)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ long temp;
+ result = 31 * result + queryExtent.hashCode();
+ temp = Double.doubleToLongBits(minSideLength);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits(queryTargetProportion);
+ result = 31 * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+ @Override
+ protected String similarityDescription() {
+ return queryExtent.toString() + "," + queryTargetProportion;
+ }
+
+ @Override
+ protected double score(Rectangle target, AtomicReference<Explanation> exp) {
+ // calculate "height": the intersection height between two boxes.
+ double top = Math.min(queryExtent.getMaxY(), target.getMaxY());
+ double bottom = Math.max(queryExtent.getMinY(), target.getMinY());
+ double height = top - bottom;
+ if (height < 0) {
+ if (exp != null) {
+ exp.set(Explanation.noMatch("No intersection"));
+ }
+ return 0;//no intersection
+ }
+
+ // calculate "width": the intersection width between two boxes.
+ double width = 0;
+ {
+ Rectangle a = queryExtent;
+ Rectangle b = target;
+ if (a.getCrossesDateLine() == b.getCrossesDateLine()) {
+ //both either cross or don't
+ double left = Math.max(a.getMinX(), b.getMinX());
+ double right = Math.min(a.getMaxX(), b.getMaxX());
+ if (!a.getCrossesDateLine()) {//both don't
+ if (left <= right) {
+ width = right - left;
+ } else if (isGeo && (Math.abs(a.getMinX()) == 180 || Math.abs(a.getMaxX()) == 180)
+ && (Math.abs(b.getMinX()) == 180 || Math.abs(b.getMaxX()) == 180)) {
+ width = 0;//both adjacent to dateline
+ } else {
+ if (exp != null) {
+ exp.set(Explanation.noMatch("No intersection"));
+ }
+ return 0;//no intersection
+ }
+ } else {//both cross
+ width = right - left + 360;
+ }
+ } else {
+ if (!a.getCrossesDateLine()) {//then flip
+ a = target;
+ b = queryExtent;
+ }
+ //a crosses, b doesn't
+ double qryWestLeft = Math.max(a.getMinX(), b.getMinX());
+ double qryWestRight = b.getMaxX();
+ if (qryWestLeft < qryWestRight)
+ width += qryWestRight - qryWestLeft;
+
+ double qryEastLeft = b.getMinX();
+ double qryEastRight = Math.min(a.getMaxX(), b.getMaxX());
+ if (qryEastLeft < qryEastRight)
+ width += qryEastRight - qryEastLeft;
+
+ if (qryWestLeft > qryWestRight && qryEastLeft > qryEastRight) {
+ if (exp != null) {
+ exp.set(Explanation.noMatch("No intersection"));
+ }
+ return 0;//no intersection
+ }
+ }
+ }
+
+ // calculate queryRatio and targetRatio
+ double intersectionArea = calcArea(width, height);
+ double queryRatio;
+ if (queryArea > 0) {
+ queryRatio = intersectionArea / queryArea;
+ } else if (queryExtent.getHeight() > 0) {//vert line
+ queryRatio = height / queryExtent.getHeight();
+ } else if (queryExtent.getWidth() > 0) {//horiz line
+ queryRatio = width / queryExtent.getWidth();
+ } else {
+ queryRatio = queryExtent.relate(target).intersects() ? 1 : 0;//could be optimized
+ }
+
+ double targetArea = calcArea(target.getWidth(), target.getHeight());
+ assert targetArea >= 0;
+ double targetRatio;
+ if (targetArea > 0) {
+ targetRatio = intersectionArea / targetArea;
+ } else if (target.getHeight() > 0) {//vert line
+ targetRatio = height / target.getHeight();
+ } else if (target.getWidth() > 0) {//horiz line
+ targetRatio = width / target.getWidth();
+ } else {
+ targetRatio = target.relate(queryExtent).intersects() ? 1 : 0;//could be optimized
+ }
+ assert queryRatio >= 0 && queryRatio <= 1 : queryRatio;
+ assert targetRatio >= 0 && targetRatio <= 1 : targetRatio;
+
+ // combine ratios into a score
+
+ double queryFactor = queryRatio * queryTargetProportion;
+ double targetFactor = targetRatio * (1.0 - queryTargetProportion);
+ double score = queryFactor + targetFactor;
+
+ if (exp!=null) {
+ String minSideDesc = minSideLength > 0.0 ? " (minSide="+minSideLength+")" : "";
+ exp.set(Explanation.match((float) score,
+ this.getClass().getSimpleName()+": queryFactor + targetFactor",
+ Explanation.match((float)intersectionArea, "IntersectionArea" + minSideDesc,
+ Explanation.match((float)width, "width"),
+ Explanation.match((float)height, "height"),
+ Explanation.match((float)queryTargetProportion, "queryTargetProportion")),
+ Explanation.match((float)queryFactor, "queryFactor",
+ Explanation.match((float)targetRatio, "ratio"),
+ Explanation.match((float)queryArea, "area of " + queryExtent + minSideDesc)),
+ Explanation.match((float)targetFactor, "targetFactor",
+ Explanation.match((float)targetRatio, "ratio"),
+ Explanation.match((float)targetArea, "area of " + target + minSideDesc))));
+ }
+
+ return score;
+ }
+
+ /** Calculates the area while applying the minimum side length. */
+ private double calcArea(double width, double height) {
+ return Math.max(minSideLength, width) * Math.max(minSideLength, height);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java
new file mode 100644
index 0000000..15cd646
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxSimilarityValueSource.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.lucene.spatial.bbox;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.queries.function.FunctionValues;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.IndexSearcher;
+
+import com.spatial4j.core.shape.Rectangle;
+
+/**
+ * A base class for calculating a spatial relevance rank per document from a provided
+ * {@link ValueSource} in which {@link FunctionValues#objectVal(int)} returns a {@link
+ * com.spatial4j.core.shape.Rectangle}.
+ * <p>
+ * Implementers: remember to implement equals and hashCode if you have
+ * fields!
+ *
+ * @lucene.experimental
+ */
+public abstract class BBoxSimilarityValueSource extends ValueSource {
+
+ private final ValueSource bboxValueSource;
+
+ public BBoxSimilarityValueSource(ValueSource bboxValueSource) {
+ this.bboxValueSource = bboxValueSource;
+ }
+
+ @Override
+ public void createWeight(Map context, IndexSearcher searcher) throws IOException {
+ bboxValueSource.createWeight(context, searcher);
+ }
+
+ @Override
+ public String description() {
+ return getClass().getSimpleName()+"(" + bboxValueSource.description() + "," + similarityDescription() + ")";
+ }
+
+ /** A comma-separated list of configurable items of the subclass to put into {@link #description()}. */
+ protected abstract String similarityDescription();
+
+ @Override
+ public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
+
+ final FunctionValues shapeValues = bboxValueSource.getValues(context, readerContext);
+
+ return new DoubleDocValues(this) {
+ @Override
+ public double doubleVal(int doc) {
+ //? limit to Rect or call getBoundingBox()? latter would encourage bad practice
+ final Rectangle rect = (Rectangle) shapeValues.objectVal(doc);
+ return rect==null ? 0 : score(rect, null);
+ }
+
+ @Override
+ public boolean exists(int doc) {
+ return shapeValues.exists(doc);
+ }
+
+ @Override
+ public Explanation explain(int doc) {
+ final Rectangle rect = (Rectangle) shapeValues.objectVal(doc);
+ if (rect == null)
+ return Explanation.noMatch("no rect");
+ AtomicReference<Explanation> explanation = new AtomicReference<>();
+ score(rect, explanation);
+ return explanation.get();
+ }
+ };
+ }
+
+ /**
+ * Return a relevancy score. If {@code exp} is provided then diagnostic information is added.
+ * @param rect The indexed rectangle; not null.
+ * @param exp Optional diagnostic holder.
+ * @return a score.
+ */
+ protected abstract double score(Rectangle rect, AtomicReference<Explanation> exp);
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;//same class
+
+ BBoxSimilarityValueSource that = (BBoxSimilarityValueSource) o;
+
+ if (!bboxValueSource.equals(that.bboxValueSource)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return bboxValueSource.hashCode();
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java
new file mode 100644
index 0000000..d5f7747
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxStrategy.java
@@ -0,0 +1,588 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.lucene.spatial.bbox;
+
+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.LegacyDoubleField;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.index.DocValuesType;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.ConstantScoreQuery;
+import org.apache.lucene.search.LegacyNumericRangeQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.spatial.SpatialStrategy;
+import org.apache.lucene.spatial.query.SpatialArgs;
+import org.apache.lucene.spatial.query.SpatialOperation;
+import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
+import org.apache.lucene.spatial.util.DistanceToShapeValueSource;
+import org.apache.lucene.util.BytesRefBuilder;
+import org.apache.lucene.util.LegacyNumericUtils;
+
+
+/**
+ * A SpatialStrategy for indexing and searching Rectangles by storing its
+ * coordinates in numeric fields. It supports all {@link SpatialOperation}s and
+ * has a custom overlap relevancy. It is based on GeoPortal's <a
+ * href="http://geoportal.svn.sourceforge.net/svnroot/geoportal/Geoportal/trunk/src/com/esri/gpt/catalog/lucene/SpatialClauseAdapter.java">SpatialClauseAdapter</a>.
+ * <p>
+ * <b>Characteristics:</b>
+ * <br>
+ * <ul>
+ * <li>Only indexes Rectangles; just one per field value. Other shapes can be provided
+ * and the bounding box will be used.</li>
+ * <li>Can query only by a Rectangle. Providing other shapes is an error.</li>
+ * <li>Supports most {@link SpatialOperation}s but not Overlaps.</li>
+ * <li>Uses the DocValues API for any sorting / relevancy.</li>
+ * </ul>
+ * <p>
+ * <b>Implementation:</b>
+ * <p>
+ * This uses 4 double fields for minX, maxX, minY, maxY
+ * and a boolean to mark a dateline cross. Depending on the particular {@link
+ * SpatialOperation}s, there are a variety of {@link org.apache.lucene.search.LegacyNumericRangeQuery}s to be
+ * done.
+ * The {@link #makeOverlapRatioValueSource(com.spatial4j.core.shape.Rectangle, double)}
+ * works by calculating the query bbox overlap percentage against the indexed
+ * shape overlap percentage. The indexed shape's coordinates are retrieved from
+ * {@link org.apache.lucene.index.LeafReader#getNumericDocValues}.
+ *
+ * @lucene.experimental
+ */
+public class BBoxStrategy extends SpatialStrategy {
+
+ public static final String SUFFIX_MINX = "__minX";
+ public static final String SUFFIX_MAXX = "__maxX";
+ public static final String SUFFIX_MINY = "__minY";
+ public static final String SUFFIX_MAXY = "__maxY";
+ public static final String SUFFIX_XDL = "__xdl";
+
+ /*
+ * The Bounding Box gets stored as four fields for x/y min/max and a flag
+ * that says if the box crosses the dateline (xdl).
+ */
+ protected final String field_bbox;
+ protected final String field_minX;
+ protected final String field_minY;
+ protected final String field_maxX;
+ protected final String field_maxY;
+ protected final String field_xdl; // crosses dateline
+
+ protected FieldType fieldType;//for the 4 numbers
+ protected FieldType xdlFieldType;
+
+ public BBoxStrategy(SpatialContext ctx, String fieldNamePrefix) {
+ super(ctx, fieldNamePrefix);
+ field_bbox = fieldNamePrefix;
+ field_minX = fieldNamePrefix + SUFFIX_MINX;
+ field_maxX = fieldNamePrefix + SUFFIX_MAXX;
+ field_minY = fieldNamePrefix + SUFFIX_MINY;
+ field_maxY = fieldNamePrefix + SUFFIX_MAXY;
+ field_xdl = fieldNamePrefix + SUFFIX_XDL;
+
+ FieldType fieldType = new FieldType(LegacyDoubleField.TYPE_NOT_STORED);
+ fieldType.setNumericPrecisionStep(8);//Solr's default
+ fieldType.setDocValuesType(DocValuesType.NUMERIC);
+ setFieldType(fieldType);
+ }
+
+ private int getPrecisionStep() {
+ return fieldType.numericPrecisionStep();
+ }
+
+ public FieldType getFieldType() {
+ return fieldType;
+ }
+
+ /** Used to customize the indexing options of the 4 number fields, and to a lesser degree the XDL field too. Search
+ * requires indexed=true, and relevancy requires docValues. If these features aren't needed then disable them.
+ * {@link FieldType#freeze()} is called on the argument. */
+ public void setFieldType(FieldType fieldType) {
+ fieldType.freeze();
+ this.fieldType = fieldType;
+ //only double's supported right now
+ if (fieldType.numericType() != FieldType.LegacyNumericType.DOUBLE)
+ throw new IllegalArgumentException("BBoxStrategy only supports doubles at this time.");
+ //for xdlFieldType, copy some similar options. Don't do docValues since it isn't needed here.
+ xdlFieldType = new FieldType(StringField.TYPE_NOT_STORED);
+ xdlFieldType.setStored(fieldType.stored());
+ xdlFieldType.setIndexOptions(fieldType.indexOptions());
+ xdlFieldType.freeze();
+ }
+
+ //---------------------------------
+ // Indexing
+ //---------------------------------
+
+ @Override
+ public Field[] createIndexableFields(Shape shape) {
+ return createIndexableFields(shape.getBoundingBox());
+ }
+
+ public Field[] createIndexableFields(Rectangle bbox) {
+ Field[] fields = new Field[5];
+ fields[0] = new ComboField(field_minX, bbox.getMinX(), fieldType);
+ fields[1] = new ComboField(field_maxX, bbox.getMaxX(), fieldType);
+ fields[2] = new ComboField(field_minY, bbox.getMinY(), fieldType);
+ fields[3] = new ComboField(field_maxY, bbox.getMaxY(), fieldType);
+ fields[4] = new ComboField(field_xdl, bbox.getCrossesDateLine()?"T":"F", xdlFieldType);
+ return fields;
+ }
+
+ /** Field subclass circumventing Field limitations. This one instance can have any combination of indexed, stored,
+ * and docValues.
+ */
+ private static class ComboField extends Field {
+ private ComboField(String name, Object value, FieldType type) {
+ super(name, type);//this expert constructor allows us to have a field that has docValues & indexed/stored
+ super.fieldsData = value;
+ }
+
+ //Is this a hack? We assume that numericValue() is only called for DocValues purposes.
+ @Override
+ public Number numericValue() {
+ //Numeric DocValues only supports Long,
+ final Number number = super.numericValue();
+ if (number == null)
+ return null;
+ if (fieldType().numericType() == FieldType.LegacyNumericType.DOUBLE)
+ return Double.doubleToLongBits(number.doubleValue());
+ if (fieldType().numericType() == FieldType.LegacyNumericType.FLOAT)
+ return Float.floatToIntBits(number.floatValue());
+ return number.longValue();
+ }
+ }
+
+ //---------------------------------
+ // Value Source / Relevancy
+ //---------------------------------
+
+ /**
+ * Provides access to each rectangle per document as a ValueSource in which
+ * {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)} returns a {@link
+ * Shape}.
+ */ //TODO raise to SpatialStrategy
+ public ValueSource makeShapeValueSource() {
+ return new BBoxValueSource(this);
+ }
+
+ @Override
+ public ValueSource makeDistanceValueSource(Point queryPoint, double multiplier) {
+ //TODO if makeShapeValueSource gets lifted to the top; this could become a generic impl.
+ return new DistanceToShapeValueSource(makeShapeValueSource(), queryPoint, multiplier, ctx);
+ }
+
+ /** Returns a similarity based on {@link BBoxOverlapRatioValueSource}. This is just a
+ * convenience method. */
+ public ValueSource makeOverlapRatioValueSource(Rectangle queryBox, double queryTargetProportion) {
+ return new BBoxOverlapRatioValueSource(
+ makeShapeValueSource(), ctx.isGeo(), queryBox, queryTargetProportion, 0.0);
+ }
+
+ //---------------------------------
+ // Query Building
+ //---------------------------------
+
+ // Utility on SpatialStrategy?
+// public Query makeQueryWithValueSource(SpatialArgs args, ValueSource valueSource) {
+// return new CustomScoreQuery(makeQuery(args), new FunctionQuery(valueSource));
+ //or...
+// return new BooleanQuery.Builder()
+// .add(new FunctionQuery(valueSource), BooleanClause.Occur.MUST)//matches everything and provides score
+// .add(filterQuery, BooleanClause.Occur.FILTER)//filters (score isn't used)
+// .build();
+// }
+
+ @Override
+ public Query makeQuery(SpatialArgs args) {
+ Shape shape = args.getShape();
+ if (!(shape instanceof Rectangle))
+ throw new UnsupportedOperationException("Can only query by Rectangle, not " + shape);
+
+ Rectangle bbox = (Rectangle) shape;
+ Query spatial;
+
+ // Useful for understanding Relations:
+ // http://edndoc.esri.com/arcsde/9.1/general_topics/understand_spatial_relations.htm
+ SpatialOperation op = args.getOperation();
+ if( op == SpatialOperation.BBoxIntersects ) spatial = makeIntersects(bbox);
+ else if( op == SpatialOperation.BBoxWithin ) spatial = makeWithin(bbox);
+ else if( op == SpatialOperation.Contains ) spatial = makeContains(bbox);
+ else if( op == SpatialOperation.Intersects ) spatial = makeIntersects(bbox);
+ else if( op == SpatialOperation.IsEqualTo ) spatial = makeEquals(bbox);
+ else if( op == SpatialOperation.IsDisjointTo ) spatial = makeDisjoint(bbox);
+ else if( op == SpatialOperation.IsWithin ) spatial = makeWithin(bbox);
+ else { //no Overlaps support yet
+ throw new UnsupportedSpatialOperation(op);
+ }
+ return new ConstantScoreQuery(spatial);
+ }
+
+ /**
+ * Constructs a query to retrieve documents that fully contain the input envelope.
+ *
+ * @return the spatial query
+ */
+ Query makeContains(Rectangle bbox) {
+
+ // general case
+ // docMinX <= queryExtent.getMinX() AND docMinY <= queryExtent.getMinY() AND docMaxX >= queryExtent.getMaxX() AND docMaxY >= queryExtent.getMaxY()
+
+ // Y conditions
+ // docMinY <= queryExtent.getMinY() AND docMaxY >= queryExtent.getMaxY()
+ Query qMinY = LegacyNumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), null, bbox.getMinY(), false, true);
+ Query qMaxY = LegacyNumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), bbox.getMaxY(), null, true, false);
+ Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);
+
+ // X conditions
+ Query xConditions;
+
+ // queries that do not cross the date line
+ if (!bbox.getCrossesDateLine()) {
+
+ // X Conditions for documents that do not cross the date line,
+ // documents that contain the min X and max X of the query envelope,
+ // docMinX <= queryExtent.getMinX() AND docMaxX >= queryExtent.getMaxX()
+ Query qMinX = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
+ Query qMaxX = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
+ Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
+ Query qNonXDL = this.makeXDL(false, qMinMax);
+
+ if (!ctx.isGeo()) {
+ xConditions = qNonXDL;
+ } else {
+ // X Conditions for documents that cross the date line,
+ // the left portion of the document contains the min X of the query
+ // OR the right portion of the document contains the max X of the query,
+ // docMinXLeft <= queryExtent.getMinX() OR docMaxXRight >= queryExtent.getMaxX()
+ Query qXDLLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
+ Query qXDLRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
+ Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qXDLLeft, qXDLRight);
+ Query qXDL = this.makeXDL(true, qXDLLeftRight);
+
+ Query qEdgeDL = null;
+ if (bbox.getMinX() == bbox.getMaxX() && Math.abs(bbox.getMinX()) == 180) {
+ double edge = bbox.getMinX() * -1;//opposite dateline edge
+ qEdgeDL = makeQuery(BooleanClause.Occur.SHOULD,
+ makeNumberTermQuery(field_minX, edge), makeNumberTermQuery(field_maxX, edge));
+ }
+
+ // apply the non-XDL and XDL conditions
+ xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL, qEdgeDL);
+ }
+ } else {
+ // queries that cross the date line
+
+ // No need to search for documents that do not cross the date line
+
+ // X Conditions for documents that cross the date line,
+ // the left portion of the document contains the min X of the query
+ // AND the right portion of the document contains the max X of the query,
+ // docMinXLeft <= queryExtent.getMinX() AND docMaxXRight >= queryExtent.getMaxX()
+ Query qXDLLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), null, bbox.getMinX(), false, true);
+ Query qXDLRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), bbox.getMaxX(), null, true, false);
+ Query qXDLLeftRight = this.makeXDL(true, this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight));
+
+ Query qWorld = makeQuery(BooleanClause.Occur.MUST,
+ makeNumberTermQuery(field_minX, -180), makeNumberTermQuery(field_maxX, 180));
+
+ xConditions = makeQuery(BooleanClause.Occur.SHOULD, qXDLLeftRight, qWorld);
+ }
+
+ // both X and Y conditions must occur
+ return this.makeQuery(BooleanClause.Occur.MUST, xConditions, yConditions);
+ }
+
+ /**
+ * Constructs a query to retrieve documents that are disjoint to the input envelope.
+ *
+ * @return the spatial query
+ */
+ Query makeDisjoint(Rectangle bbox) {
+
+ // general case
+ // docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX() OR docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()
+
+ // Y conditions
+ // docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()
+ Query qMinY = LegacyNumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), bbox.getMaxY(), null, false, false);
+ Query qMaxY = LegacyNumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), null, bbox.getMinY(), false, false);
+ Query yConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qMinY, qMaxY);
+
+ // X conditions
+ Query xConditions;
+
+ // queries that do not cross the date line
+ if (!bbox.getCrossesDateLine()) {
+
+ // X Conditions for documents that do not cross the date line,
+ // docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()
+ Query qMinX = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
+ if (bbox.getMinX() == -180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
+ BooleanQuery.Builder bq = new BooleanQuery.Builder();
+ bq.add(qMinX, BooleanClause.Occur.MUST);
+ bq.add(makeNumberTermQuery(field_maxX, 180.0), BooleanClause.Occur.MUST_NOT);
+ qMinX = bq.build();
+ }
+ Query qMaxX = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
+ if (bbox.getMaxX() == 180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
+ BooleanQuery.Builder bq = new BooleanQuery.Builder();
+ bq.add(qMaxX, BooleanClause.Occur.MUST);
+ bq.add(makeNumberTermQuery(field_minX, -180.0), BooleanClause.Occur.MUST_NOT);
+ qMaxX = bq.build();
+ }
+ Query qMinMax = this.makeQuery(BooleanClause.Occur.SHOULD, qMinX, qMaxX);
+ Query qNonXDL = this.makeXDL(false, qMinMax);
+
+ if (!ctx.isGeo()) {
+ xConditions = qNonXDL;
+ } else {
+ // X Conditions for documents that cross the date line,
+
+ // both the left and right portions of the document must be disjoint to the query
+ // (docMinXLeft > queryExtent.getMaxX() OR docMaxXLeft < queryExtent.getMinX()) AND
+ // (docMinXRight > queryExtent.getMaxX() OR docMaxXRight < queryExtent.getMinX())
+ // where: docMaxXLeft = 180.0, docMinXRight = -180.0
+ // (docMaxXLeft < queryExtent.getMinX()) equates to (180.0 < queryExtent.getMinX()) and is ignored
+ // (docMinXRight > queryExtent.getMaxX()) equates to (-180.0 > queryExtent.getMaxX()) and is ignored
+ Query qMinXLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
+ Query qMaxXRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
+ Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXRight);
+ Query qXDL = this.makeXDL(true, qLeftRight);
+
+ // apply the non-XDL and XDL conditions
+ xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
+ }
+ // queries that cross the date line
+ } else {
+
+ // X Conditions for documents that do not cross the date line,
+ // the document must be disjoint to both the left and right query portions
+ // (docMinX > queryExtent.getMaxX()Left OR docMaxX < queryExtent.getMinX()) AND (docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()Left)
+ // where: queryExtent.getMaxX()Left = 180.0, queryExtent.getMinX()Left = -180.0
+ Query qMinXLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), 180.0, null, false, false);
+ Query qMaxXLeft = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMinX(), false, false);
+ Query qMinXRight = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMaxX(), null, false, false);
+ Query qMaxXRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, -180.0, false, false);
+ Query qLeft = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXLeft, qMaxXLeft);
+ Query qRight = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXRight, qMaxXRight);
+ Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qLeft, qRight);
+
+ // No need to search for documents that do not cross the date line
+
+ xConditions = this.makeXDL(false, qLeftRight);
+ }
+
+ // either X or Y conditions should occur
+ return this.makeQuery(BooleanClause.Occur.SHOULD, xConditions, yConditions);
+ }
+
+ /**
+ * Constructs a query to retrieve documents that equal the input envelope.
+ *
+ * @return the spatial query
+ */
+ Query makeEquals(Rectangle bbox) {
+
+ // docMinX = queryExtent.getMinX() AND docMinY = queryExtent.getMinY() AND docMaxX = queryExtent.getMaxX() AND docMaxY = queryExtent.getMaxY()
+ Query qMinX = makeNumberTermQuery(field_minX, bbox.getMinX());
+ Query qMinY = makeNumberTermQuery(field_minY, bbox.getMinY());
+ Query qMaxX = makeNumberTermQuery(field_maxX, bbox.getMaxX());
+ Query qMaxY = makeNumberTermQuery(field_maxY, bbox.getMaxY());
+ return makeQuery(BooleanClause.Occur.MUST, qMinX, qMinY, qMaxX, qMaxY);
+ }
+
+ /**
+ * Constructs a query to retrieve documents that intersect the input envelope.
+ *
+ * @return the spatial query
+ */
+ Query makeIntersects(Rectangle bbox) {
+
+ // the original intersects query does not work for envelopes that cross the date line,
+ // switch to a NOT Disjoint query
+
+ // MUST_NOT causes a problem when it's the only clause type within a BooleanQuery,
+ // to get around it we add all documents as a SHOULD
+
+ // there must be an envelope, it must not be disjoint
+ Query qHasEnv;
+ if (ctx.isGeo()) {
+ Query qIsNonXDL = this.makeXDL(false);
+ Query qIsXDL = ctx.isGeo() ? this.makeXDL(true) : null;
+ qHasEnv = this.makeQuery(BooleanClause.Occur.SHOULD, qIsNonXDL, qIsXDL);
+ } else {
+ qHasEnv = this.makeXDL(false);
+ }
+
+ BooleanQuery.Builder qNotDisjoint = new BooleanQuery.Builder();
+ qNotDisjoint.add(qHasEnv, BooleanClause.Occur.MUST);
+ Query qDisjoint = makeDisjoint(bbox);
+ qNotDisjoint.add(qDisjoint, BooleanClause.Occur.MUST_NOT);
+
+ //Query qDisjoint = makeDisjoint();
+ //BooleanQuery qNotDisjoint = new BooleanQuery();
+ //qNotDisjoint.add(new MatchAllDocsQuery(),BooleanClause.Occur.SHOULD);
+ //qNotDisjoint.add(qDisjoint,BooleanClause.Occur.MUST_NOT);
+ return qNotDisjoint.build();
+ }
+
+ /**
+ * Makes a boolean query based upon a collection of queries and a logical operator.
+ *
+ * @param occur the logical operator
+ * @param queries the query collection
+ * @return the query
+ */
+ BooleanQuery makeQuery(BooleanClause.Occur occur, Query... queries) {
+ BooleanQuery.Builder bq = new BooleanQuery.Builder();
+ for (Query query : queries) {
+ if (query != null)
+ bq.add(query, occur);
+ }
+ return bq.build();
+ }
+
+ /**
+ * Constructs a query to retrieve documents are fully within the input envelope.
+ *
+ * @return the spatial query
+ */
+ Query makeWithin(Rectangle bbox) {
+
+ // general case
+ // docMinX >= queryExtent.getMinX() AND docMinY >= queryExtent.getMinY() AND docMaxX <= queryExtent.getMaxX() AND docMaxY <= queryExtent.getMaxY()
+
+ // Y conditions
+ // docMinY >= queryExtent.getMinY() AND docMaxY <= queryExtent.getMaxY()
+ Query qMinY = LegacyNumericRangeQuery.newDoubleRange(field_minY, getPrecisionStep(), bbox.getMinY(), null, true, false);
+ Query qMaxY = LegacyNumericRangeQuery.newDoubleRange(field_maxY, getPrecisionStep(), null, bbox.getMaxY(), false, true);
+ Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);
+
+ // X conditions
+ Query xConditions;
+
+ if (ctx.isGeo() && bbox.getMinX() == -180.0 && bbox.getMaxX() == 180.0) {
+ //if query world-wraps, only the y condition matters
+ return yConditions;
+
+ } else if (!bbox.getCrossesDateLine()) {
+ // queries that do not cross the date line
+
+ // docMinX >= queryExtent.getMinX() AND docMaxX <= queryExtent.getMaxX()
+ Query qMinX = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
+ Query qMaxX = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
+ Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
+
+ double edge = 0;//none, otherwise opposite dateline of query
+ if (bbox.getMinX() == -180.0)
+ edge = 180;
+ else if (bbox.getMaxX() == 180.0)
+ edge = -180;
+ if (edge != 0 && ctx.isGeo()) {
+ Query edgeQ = makeQuery(BooleanClause.Occur.MUST,
+ makeNumberTermQuery(field_minX, edge), makeNumberTermQuery(field_maxX, edge));
+ qMinMax = makeQuery(BooleanClause.Occur.SHOULD, qMinMax, edgeQ);
+ }
+
+ xConditions = this.makeXDL(false, qMinMax);
+
+ // queries that cross the date line
+ } else {
+
+ // X Conditions for documents that do not cross the date line
+
+ // the document should be within the left portion of the query
+ // docMinX >= queryExtent.getMinX() AND docMaxX <= 180.0
+ Query qMinXLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
+ Query qMaxXLeft = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, 180.0, false, true);
+ Query qLeft = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXLeft);
+
+ // the document should be within the right portion of the query
+ // docMinX >= -180.0 AND docMaxX <= queryExtent.getMaxX()
+ Query qMinXRight = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), -180.0, null, true, false);
+ Query qMaxXRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
+ Query qRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXRight, qMaxXRight);
+
+ // either left or right conditions should occur,
+ // apply the left and right conditions to documents that do not cross the date line
+ Query qLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qLeft, qRight);
+ Query qNonXDL = this.makeXDL(false, qLeftRight);
+
+ // X Conditions for documents that cross the date line,
+ // the left portion of the document must be within the left portion of the query,
+ // AND the right portion of the document must be within the right portion of the query
+ // docMinXLeft >= queryExtent.getMinX() AND docMaxXLeft <= 180.0
+ // AND docMinXRight >= -180.0 AND docMaxXRight <= queryExtent.getMaxX()
+ Query qXDLLeft = LegacyNumericRangeQuery.newDoubleRange(field_minX, getPrecisionStep(), bbox.getMinX(), null, true, false);
+ Query qXDLRight = LegacyNumericRangeQuery.newDoubleRange(field_maxX, getPrecisionStep(), null, bbox.getMaxX(), false, true);
+ Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight);
+ Query qXDL = this.makeXDL(true, qXDLLeftRight);
+
+ // apply the non-XDL and XDL conditions
+ xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
+ }
+
+ // both X and Y conditions must occur
+ return this.makeQuery(BooleanClause.Occur.MUST, xConditions, yConditions);
+ }
+
+ /**
+ * Constructs a query to retrieve documents that do or do not cross the date line.
+ *
+ * @param crossedDateLine <code>true</true> for documents that cross the date line
+ * @return the query
+ */
+ private Query makeXDL(boolean crossedDateLine) {
+ // The 'T' and 'F' values match solr fields
+ return new TermQuery(new Term(field_xdl, crossedDateLine ? "T" : "F"));
+ }
+
+ /**
+ * Constructs a query to retrieve documents that do or do not cross the date line
+ * and match the supplied spatial query.
+ *
+ * @param crossedDateLine <code>true</true> for documents that cross the date line
+ * @param query the spatial query
+ * @return the query
+ */
+ private Query makeXDL(boolean crossedDateLine, Query query) {
+ if (!ctx.isGeo()) {
+ assert !crossedDateLine;
+ return query;
+ }
+ BooleanQuery.Builder bq = new BooleanQuery.Builder();
+ bq.add(this.makeXDL(crossedDateLine), BooleanClause.Occur.MUST);
+ bq.add(query, BooleanClause.Occur.MUST);
+ return bq.build();
+ }
+
+ private Query makeNumberTermQuery(String field, double number) {
+ BytesRefBuilder bytes = new BytesRefBuilder();
+ LegacyNumericUtils.longToPrefixCodedBytes(LegacyNumericUtils.doubleToSortableLong(number), 0, bytes);
+ return new TermQuery(new Term(field, bytes.get()));
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java
new file mode 100644
index 0000000..5d95407
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/BBoxValueSource.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.lucene.spatial.bbox;
+
+import com.spatial4j.core.shape.Rectangle;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.DocValues;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.queries.function.FunctionValues;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.util.Bits;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * A ValueSource in which the indexed Rectangle is returned from
+ * {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)}.
+ *
+ * @lucene.internal
+ */
+class BBoxValueSource extends ValueSource {
+
+ private final BBoxStrategy strategy;
+
+ public BBoxValueSource(BBoxStrategy strategy) {
+ this.strategy = strategy;
+ }
+
+ @Override
+ public String description() {
+ return "bboxShape(" + strategy.getFieldName() + ")";
+ }
+
+ @Override
+ public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
+ LeafReader reader = readerContext.reader();
+ final NumericDocValues minX = DocValues.getNumeric(reader, strategy.field_minX);
+ final NumericDocValues minY = DocValues.getNumeric(reader, strategy.field_minY);
+ final NumericDocValues maxX = DocValues.getNumeric(reader, strategy.field_maxX);
+ final NumericDocValues maxY = DocValues.getNumeric(reader, strategy.field_maxY);
+
+ final Bits validBits = DocValues.getDocsWithField(reader, strategy.field_minX);//could have chosen any field
+ //reused
+ final Rectangle rect = strategy.getSpatialContext().makeRectangle(0,0,0,0);
+
+ return new FunctionValues() {
+ @Override
+ public Object objectVal(int doc) {
+ if (!validBits.get(doc)) {
+ return null;
+ } else {
+ rect.reset(
+ Double.longBitsToDouble(minX.get(doc)), Double.longBitsToDouble(maxX.get(doc)),
+ Double.longBitsToDouble(minY.get(doc)), Double.longBitsToDouble(maxY.get(doc)));
+ return rect;
+ }
+ }
+
+ @Override
+ public String strVal(int doc) {//TODO support WKT output once Spatial4j does
+ Object v = objectVal(doc);
+ return v == null ? null : v.toString();
+ }
+
+ @Override
+ public boolean exists(int doc) {
+ return validBits.get(doc);
+ }
+
+ @Override
+ public Explanation explain(int doc) {
+ return Explanation.match(Float.NaN, toString(doc));
+ }
+
+ @Override
+ public String toString(int doc) {
+ return description() + '=' + strVal(doc);
+ }
+ };
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ BBoxValueSource that = (BBoxValueSource) o;
+
+ if (!strategy.equals(that.strategy)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return strategy.hashCode();
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5b028349/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java
----------------------------------------------------------------------
diff --git a/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java
new file mode 100644
index 0000000..a229311
--- /dev/null
+++ b/lucene/spatial-extras/src/java/org/apache/lucene/spatial/bbox/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.
+ */
+/**
+ * Bounding Box Spatial Strategy
+ * <p>
+ * Index a shape extent using 4 numeric fields and a flag to say if it crosses the dateline
+ */
+package org.apache.lucene.spatial.bbox;
\ No newline at end of file