/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
* (C) 2010, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.index.quadtree;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotoolkit.index.CloseableCollection;
import org.geotoolkit.index.Data;
import org.apache.sis.util.logging.Logging;
import com.vividsolutions.jts.geom.Envelope;
/**
* Java porting of mapserver quadtree implementation.<br>
* <br>
* Note that this implementation is <b>not thread safe</b>, so don't share the
* same instance across two or more threads.
*
* TODO: example of typical use...
*
* @author Tommaso Nolli
* @module
*/
public class QuadTree {
private static final double SPLITRATIO = 0.55d;
public static final Logger LOGGER = Logging.getLogger("org.geotoolkit.index.quadtree");
//open iterators
private final Set iterators = new HashSet();
private AbstractNode root;
private int numShapes;
private int maxDepth;
/**
* Constructor. The maxDepth will be calculated.
*
* @param numShapes
* The total number of shapes to index
* @param maxBounds
* The bounds of all geometries to be indexed
*/
public QuadTree(final int numShapes, final Envelope maxBounds) {
this(numShapes, 0, maxBounds);
}
/**
* Constructor. WARNING: using this constructor, you have to manually set
* the root
*
* @param numShapes
* The total number of shapes to index
* @param maxDepth
* The max depth of the index, must be <= 65535
*/
public QuadTree(final int numShapes, final int maxDepth) {
this(numShapes, maxDepth, null);
}
/**
* Constructor.
*
* @param numShapes
* The total number of shapes to index
* @param maxDepth
* The max depth of the index, must be <= 65535
* @param maxBounds
* The bounds of all geometries to be indexed
*/
public QuadTree(final int numShapes, final int maxDepth, final Envelope maxBounds) {
if (maxDepth > 65535) {
throw new IllegalArgumentException("maxDepth must be <= 65535 value is " + maxDepth);
}
this.numShapes = numShapes;
this.maxDepth = maxDepth;
if (maxBounds != null){
this.root = new Node(maxBounds.getMinX(), maxBounds.getMinY(),
maxBounds.getMaxX(), maxBounds.getMaxY());
}
if (maxDepth < 1){
/*
* No max depth was defined, try to select a reasonable one that
* implies approximately 8 shapes per node.
*/
int numNodes = 1;
this.maxDepth = 0;
while (numNodes * 4 < numShapes) {
this.maxDepth += 1;
numNodes = numNodes * 2;
}
}
}
/**
* Will cause the tree to explore every node.
*/
public void loadAll() throws StoreException{
load(getRoot());
}
private void load(final AbstractNode node) throws StoreException{
for(int i=0, n=node.getNumSubNodes(); i<n; i++){
load(node.getSubNode(i));
}
}
/**
* Inserts a shape record id in the quadtree
*
* @param recno
* The record number
* @param bounds
* The bounding box
*/
public void insert(final int recno, final Envelope bounds) throws StoreException {
this.insert(this.root, recno, bounds, this.maxDepth);
}
/**
* Inserts a shape record id in the quadtree
*
* @param node
* @param recno
* @param bounds
* @param md
* @throws StoreException
*/
private void insert(final AbstractNode node, final int recno, final Envelope bounds, final int md)
throws StoreException {
final Envelope buffer = new Envelope();
if (md > 1 && node.getNumSubNodes() > 0) {
/*
* If there are subnodes, then consider whether this object will fit
* in them.
*/
AbstractNode subNode = null;
for (int i = 0; i < node.getNumSubNodes(); i++) {
subNode = node.getSubNode(i);
if (subNode.getBounds(buffer).contains(bounds)) {
this.insert(subNode, recno, bounds, md - 1);
return;
}
}
} else if (md > 1 && node.getNumSubNodes() == 0) {
/*
* Otherwise, consider creating four subnodes if could fit into
* them, and adding to the appropriate subnode.
*/
final Envelope[] quads = this.splitBounds(node.getBounds(buffer));
if (quads[0].contains(bounds) || quads[1].contains(bounds)
|| quads[2].contains(bounds) || quads[3].contains(bounds)) {
node.setSubNodes(new Node[]{
new Node(quads[0]),
new Node(quads[1]),
new Node(quads[2]),
new Node(quads[3])
});
// recurse back on this node now that it has subnodes
this.insert(node, recno, bounds, md);
return;
}
}
// If none of that worked, just add it to this nodes list.
node.addShapeId(recno);
}
/**
*
* @param bounds
* @return A List of Integer
*/
public <T extends Data> CloseableCollection<T> search(final DataReader<T> reader, final Envelope bounds) throws StoreException {
return search(reader,bounds,null);
}
/**
*
* @param bounds
* @param minRes : nodes with a small envelope then the given resolution will be ignored.
* @return A List of Integer
*/
public <T extends Data> CloseableCollection<T> search(final DataReader<T> reader,final Envelope bounds,
final double[] minRes) throws StoreException {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "Querying {0}", bounds);
}
LazySearchCollection lazySearchCollection;
try {
lazySearchCollection = new LazySearchCollection(this, reader, bounds, minRes);
} catch (RuntimeException e) {
LOGGER.warning("IOException occurred while reading root");
return null;
}
return lazySearchCollection;
}
/**
* Closes this QuadTree after use...
*
* @throws StoreException
*/
public void close(final Iterator iter) throws StoreException {
iterators.remove(iter);
if (iter instanceof SearchIterator){
((SearchIterator) iter).close();
}
}
/**
*
*/
public boolean trim() throws StoreException {
LOGGER.fine("Trimming the tree...");
return this.trim(this.root);
}
/**
* Trim subtrees, and free subnodes that come back empty.
*
* @param node The node to trim
* @return true if this node has been trimmed
*/
private boolean trim(final AbstractNode node) throws StoreException {
final int nbSub = node.getNumSubNodes();
if(nbSub>0){
final List<AbstractNode> dummy = new ArrayList<AbstractNode>(nbSub);
for (int i=0; i<nbSub; i++) {
final AbstractNode n = node.getSubNode(i);
if(!this.trim(n)) {
dummy.add(n);
}
}
node.setSubNodes(dummy.toArray(new Node[dummy.size()]));
}
/*
* If I have only 1 subnode and no shape records, promote that subnode
* to my position.
*/
if (node.getNumSubNodes() == 1 && node.getNumShapeIds() == 0) {
final AbstractNode subNode = node.getSubNode(0);
final int nbssn = subNode.getNumSubNodes();
final AbstractNode[] ssn = new AbstractNode[nbssn];
for(int i=0;i<nbssn;i++){
ssn[i] = subNode.getSubNode(i);
}
node.setSubNodes(ssn);
node.setShapesId(subNode.getShapesId());
node.setEnvelope(subNode.getEnvelope());
}
return (node.getNumSubNodes() == 0 && node.getNumShapeIds() == 0);
}
/**
* Splits the specified Envelope
*
* @param in an Envelope to split
* @return an array of 4 Envelopes
* +---+---+
* | 0 | 1 |
* +---+---+
* | 2 | 3 |
* +---+---+
*/
private Envelope[] splitBounds(final Envelope in) {
final Envelope[] ret = new Envelope[4];
final double minx = in.getMinX();
final double miny = in.getMinY();
final double maxx = in.getMaxX();
final double maxy = in.getMaxY();
final double middlex = minx + (in.getWidth()/2);
final double middley = miny + (in.getHeight()/2);
ret[0] = new Envelope(minx, middlex, middley, maxy);
ret[1] = new Envelope(middlex, maxx, middley, maxy);
ret[2] = new Envelope(minx, middlex, miny, middley);
ret[3] = new Envelope(middlex, maxx, miny, middley);
return ret;
}
/**
* @return Returns the maxDepth.
*/
public int getMaxDepth() {
return this.maxDepth;
}
/**
* @param maxDepth
* The maxDepth to set.
*/
public void setMaxDepth(final int maxDepth) {
this.maxDepth = maxDepth;
}
/**
* @return Returns the numShapes.
*/
public int getNumShapes() {
return this.numShapes;
}
/**
* @param numShapesAbstractNode
* The numShapes to set.
*/
public void setNumShapes(final int numShapes) {
this.numShapes = numShapes;
}
/**
* @return Returns the root.
*/
public AbstractNode getRoot() {
return this.root;
}
/**
* @param root
* The root to set.
*/
public void setRoot(final AbstractNode root) {
this.root = root;
}
public void close() throws StoreException {
if (!iterators.isEmpty()) {
throw new StoreException("There are still open iterators!!");
}
}
public void registerIterator(final Iterator object) {
iterators.add(object);
}
}