/* * 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 java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.MockAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks; import org.locationtech.spatial4j.context.SpatialContext; import org.locationtech.spatial4j.distance.DistanceUtils; import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Rectangle; import static com.carrotsearch.randomizedtesting.RandomizedTest.randomDouble; import static com.carrotsearch.randomizedtesting.RandomizedTest.randomGaussian; import static com.carrotsearch.randomizedtesting.RandomizedTest.randomInt; import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween; /** A base test class for spatial lucene. It's mostly Lucene generic. */ @SuppressSysoutChecks(bugUrl = "These tests use JUL extensively.") public abstract class SpatialTestCase extends LuceneTestCase { protected Logger log = Logger.getLogger(getClass().getName()); private DirectoryReader indexReader; protected RandomIndexWriter indexWriter; private Directory directory; private Analyzer analyzer; protected IndexSearcher indexSearcher; protected SpatialContext ctx;//subclass must initialize @Override public void setUp() throws Exception { super.setUp(); directory = newDirectory(); analyzer = new MockAnalyzer(random()); indexWriter = new RandomIndexWriter(random(), directory, LuceneTestCase.newIndexWriterConfig(random(), analyzer)); indexReader = indexWriter.getReader(); indexSearcher = newSearcher(indexReader); } @Override public void tearDown() throws Exception { IOUtils.close(indexWriter, indexReader, analyzer, directory); super.tearDown(); } // ================================================= Helper Methods ================================================ protected void addDocument(Document doc) throws IOException { indexWriter.addDocument(doc); } protected void addDocumentsAndCommit(List<Document> documents) throws IOException { for (Document document : documents) { indexWriter.addDocument(document); } commit(); } protected void deleteAll() throws IOException { indexWriter.deleteAll(); } protected void commit() throws IOException { indexWriter.commit(); DirectoryReader newReader = DirectoryReader.openIfChanged(indexReader); if (newReader != null) { IOUtils.close(indexReader); indexReader = newReader; } indexSearcher = newSearcher(indexReader); } protected void verifyDocumentsIndexed(int numDocs) { assertEquals(numDocs, indexReader.numDocs()); } protected SearchResults executeQuery(Query query, int numDocs) { try { TopDocs topDocs = indexSearcher.search(query, numDocs); List<SearchResult> results = new ArrayList<>(); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { results.add(new SearchResult(scoreDoc.score, indexSearcher.doc(scoreDoc.doc))); } return new SearchResults(topDocs.totalHits, results); } catch (IOException ioe) { throw new RuntimeException("IOException thrown while executing query", ioe); } } protected Point randomPoint() { final Rectangle WB = ctx.getWorldBounds(); return ctx.makePoint( randomIntBetween((int) WB.getMinX(), (int) WB.getMaxX()), randomIntBetween((int) WB.getMinY(), (int) WB.getMaxY())); } protected Rectangle randomRectangle() { return randomRectangle(ctx.getWorldBounds()); } protected Rectangle randomRectangle(Rectangle bounds) { double[] xNewStartAndWidth = randomSubRange(bounds.getMinX(), bounds.getWidth()); double xMin = xNewStartAndWidth[0]; double xMax = xMin + xNewStartAndWidth[1]; if (bounds.getCrossesDateLine()) { xMin = DistanceUtils.normLonDEG(xMin); xMax = DistanceUtils.normLonDEG(xMax); } double[] yNewStartAndHeight = randomSubRange(bounds.getMinY(), bounds.getHeight()); double yMin = yNewStartAndHeight[0]; double yMax = yMin + yNewStartAndHeight[1]; return ctx.makeRectangle(xMin, xMax, yMin, yMax); } /** Returns new minStart and new length that is inside the range specified by the arguments. */ protected double[] randomSubRange(double boundStart, double boundLen) { if (boundLen >= 3 && usually()) { // typical // prefer integers for ease of debugability ... and prefer 1/16th of bound int intBoundStart = (int) Math.ceil(boundStart); int intBoundEnd = (int) (boundStart + boundLen); int intBoundLen = intBoundEnd - intBoundStart; int newLen = (int) randomGaussianMeanMax(intBoundLen / 16.0, intBoundLen); int newStart = intBoundStart + randomInt(intBoundLen - newLen); return new double[]{newStart, newLen}; } else { // (no int rounding) double newLen = randomGaussianMeanMax(boundLen / 16, boundLen); double newStart = boundStart + (boundLen - newLen == 0 ? 0 : (randomDouble() % (boundLen - newLen))); return new double[]{newStart, newLen}; } } private double randomGaussianMinMeanMax(double min, double mean, double max) { assert mean > min; return randomGaussianMeanMax(mean - min, max - min) + min; } /** * Within one standard deviation (68% of the time) the result is "close" to * mean. By "close": when greater than mean, it's the lesser of 2*mean or half * way to max, when lesser than mean, it's the greater of max-2*mean or half * way to 0. The other 32% of the time it's in the rest of the range, touching * either 0 or max but never exceeding. */ private double randomGaussianMeanMax(double mean, double max) { // DWS: I verified the results empirically assert mean <= max && mean >= 0; double g = randomGaussian(); double mean2 = mean; double flip = 1; if (g < 0) { mean2 = max - mean; flip = -1; g *= -1; } // pivot is the distance from mean2 towards max where the boundary of // 1 standard deviation alters the calculation double pivotMax = max - mean2; double pivot = Math.min(mean2, pivotMax / 2);//from 0 to max-mean2 assert pivot >= 0 && pivotMax >= pivot && g >= 0; double pivotResult; if (g <= 1) pivotResult = pivot * g; else pivotResult = Math.min(pivotMax, (g - 1) * (pivotMax - pivot) + pivot); double result = mean + flip * pivotResult; return (result < 0 || result > max) ? mean : result; // due this due to computational numerical precision } // ================================================= Inner Classes ================================================= protected static class SearchResults { public int numFound; public List<SearchResult> results; public SearchResults(int numFound, List<SearchResult> results) { this.numFound = numFound; this.results = results; } public StringBuilder toDebugString() { StringBuilder str = new StringBuilder(); str.append("found: ").append(numFound).append('['); for(SearchResult r : results) { String id = r.getId(); str.append(id).append(", "); } str.append(']'); return str; } @Override public String toString() { return "[found:"+numFound+" "+results+"]"; } } protected static class SearchResult { public float score; public Document document; public SearchResult(float score, Document document) { this.score = score; this.document = document; } public String getId() { return document.get("id"); } @Override public String toString() { return "["+score+"="+document+"]"; } } }