/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.caching.grid.spatialindex;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import org.geotools.caching.EvictableTree;
import org.geotools.caching.EvictionPolicy;
import org.geotools.caching.LRUEvictionPolicy;
import org.geotools.caching.grid.featurecache.GridFeatureCache;
import org.geotools.caching.grid.spatialindex.store.StorageFactory;
import org.geotools.caching.spatialindex.AbstractSpatialIndex;
import org.geotools.caching.spatialindex.Node;
import org.geotools.caching.spatialindex.NodeIdentifier;
import org.geotools.caching.spatialindex.Region;
import org.geotools.caching.spatialindex.RegionNodeIdentifier;
import org.geotools.caching.spatialindex.Shape;
import org.geotools.caching.spatialindex.SpatialIndex;
import org.geotools.caching.spatialindex.Storage;
import org.geotools.caching.spatialindex.Visitor;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
/** A grid implementation of SpatialIndex.
* A grid is a regular division of space, and is implemented as a very simple tree.
* It has two levels, a top level consisting of one root node, and
* a bottom level of nodes of the same size forming a grid.
* Data is either inserted at the top level or at the bottom level,
* and may be inserted more than once, if data intersects more than one node.
* If data's shape is too big, it is inserted at the top level.
* For the grid to be efficient, data should evenly distributed in size and in space,
* and grid size should twice the mean size of data's shape.
*
* @author Christophe Rousson, SoC 2007, CRG-ULAVAL
*
*
* @source $URL$
*/
public class GridSpatialIndex extends AbstractSpatialIndex implements EvictableTree {
public static final String GRID_SIZE_PROPERTY = "Grid.GridSize";
public static final String GRID_CAPACITY_PROPERTY = "Grid.GridCapacity";
public static final String ROOT_MBR_MINX_PROPERTY = "Grid.RootMbrMinX";
public static final String ROOT_MBR_MINY_PROPERTY = "Grid.RootMbrMinY";
public static final String ROOT_MBR_MAXX_PROPERTY = "Grid.RootMbrMaxX";
public static final String ROOT_MBR_MAXY_PROPERTY = "Grid.RootMbrMaxY";
protected int MAX_INSERTION = 4;
protected int gridsize;
private int featureCapacity;
protected Region mbr;
private EvictionPolicy policy;
private boolean doRecordAccess = true;
/** Constructor. Creates a new Grid covering space given by <code>mbr</code>
* and with at least <code>capacity</code> nodes.
*
* @param mbr
* @param capacity - the number of tiles in the index
* @param store - the backend index storage
*/
public GridSpatialIndex(Region mbr, int gridsize, Storage store, int capacity) {
this.gridsize = gridsize;
this.mbr = mbr;
this.store = store;
this.stats = new GridSpatialIndexStatistics();
this.policy = new LRUEvictionPolicy(this);
this.featureCapacity = capacity;
this.root = null;
try{
initializeFromStorage(this.store);
}catch (Exception ex){
//ignore any errors and move on
}
if (this.root == null){
this.dimension = mbr.getDimension();
//nothing read from storage so we need to create new ones
this.store.clear();
this.root = findUniqueInstance(new RegionNodeIdentifier(mbr));
GridRootNode root = new GridRootNode(gridsize, (RegionNodeIdentifier)this.root);
root.split(this);
writeNode(root);
this.stats.addToNodesCounter(root.getCapacity() + 1); // root has root.capacity nodes, +1 for root itself :)
}
}
protected GridSpatialIndex() {
}
public static SpatialIndex createInstance(Properties pset) {
Storage storage = StorageFactory.getInstance().createStorage(pset);
int gridsize = Integer.parseInt(pset.getProperty(GRID_SIZE_PROPERTY));
int gridcapacity = Integer.parseInt(pset.getProperty(GRID_CAPACITY_PROPERTY));
double minx = Double.parseDouble(pset.getProperty(ROOT_MBR_MINX_PROPERTY));
double miny = Double.parseDouble(pset.getProperty(ROOT_MBR_MINY_PROPERTY));
double maxx = Double.parseDouble(pset.getProperty(ROOT_MBR_MAXX_PROPERTY));
double maxy = Double.parseDouble(pset.getProperty(ROOT_MBR_MAXY_PROPERTY));
Region mbr = new Region(new double[] { minx, miny }, new double[] { maxx, maxy });
GridSpatialIndex instance = new GridSpatialIndex(mbr, gridsize, storage, gridcapacity);
return instance;
}
/**
*
* @return the root node of the grid
*/
public GridRootNode getRootNode() {
return (GridRootNode) this.rootNode;
}
public void dispose(){
this.store.dispose();
}
protected void visitData(Node n, Visitor v, Shape query, int type) {
GridNode node = (GridNode) n;
if (type == GridSpatialIndex.IntersectionQuery){
for (Iterator<GridData> it = node.data.iterator(); it.hasNext();) {
GridData d = it.next();
if (query.intersects(d.getShape())){
v.visitData(d);
}
}
}else if (type == GridSpatialIndex.ContainmentQuery){
for (Iterator<GridData> it = node.data.iterator(); it.hasNext();) {
GridData d = it.next();
if (query.contains(d.getShape())){
v.visitData(d);
}
}
}
}
public void clear() throws IllegalStateException {
// we drop all nodes and recreate grid ; GC will do the rest
this.store.clear();
//create a new root node
this.root = findUniqueInstance(new RegionNodeIdentifier(this.mbr));
GridRootNode root = new GridRootNode(gridsize, (RegionNodeIdentifier)this.root);
root.split(this);
writeNode(root);
this.stats.reset();
this.stats.addToNodesCounter(root.getCapacity() + 1); // root has root.capacity nodes, +1 for root itself :)
this.flush();
}
public Properties getIndexProperties() {
Properties pset = store.getPropertySet();
pset.setProperty(INDEX_TYPE_PROPERTY, GridSpatialIndex.class.getCanonicalName());
pset.setProperty(GRID_SIZE_PROPERTY, new Integer(gridsize).toString());
pset.setProperty(GRID_CAPACITY_PROPERTY, new Integer(featureCapacity).toString());
pset.setProperty(ROOT_MBR_MINX_PROPERTY, new Double(mbr.getLow(0)).toString());
pset.setProperty(ROOT_MBR_MINY_PROPERTY, new Double(mbr.getLow(1)).toString());
pset.setProperty(ROOT_MBR_MAXX_PROPERTY, new Double(mbr.getHigh(0)).toString());
pset.setProperty(ROOT_MBR_MAXY_PROPERTY, new Double(mbr.getHigh(1)).toString());
return pset;
}
private boolean insertDataToNode(GridNode node, Object data, Shape shape){
GridData gd = new GridData(shape, data);
if (node.getIdentifier().isWritable() && node.insertData(gd)) {
writeNode(node);
return true;
}
return false;
}
private boolean insertDataToNodeID(NodeIdentifier n, Object data, Shape shape) {
if (!n.isValid()) return false; //no point in writing to an invalid node
GridNode node = (GridNode)readNode(n);
return insertDataToNode(node, data, shape);
}
protected void insertData(NodeIdentifier n, Object data, Shape shape) {
/* so we prefer this version :
* data may be inserted more than one time, in each tile intersecting data's MBR.
* However, very big MBR will cause data to be inserted in a large number of tiles :
* given a threshold, data is inserted at root node.
* */
NodeCursor cc = new NodeCursor(getRootNode(),shape);
boolean added = false;
if (cc.getChildCount() > MAX_INSERTION) {
GridRootNode node = getRootNode();
added = insertDataToNode(node, data, shape);
} else {
NodeIdentifier next = null;
while( (next = cc.getNext()) != null ){
if (insertDataToNodeID(next, data, shape)){
added = true;
}
}
}
if (added){
this.stats.addToDataCounter(1); //even though the feature may be added to multiple nodes; it only counts as one more feature in the cache.
String x= "abc";
}
}
protected void insertDataOutOfBounds(Object data, Shape shape) {
throw new IllegalArgumentException("Grids cannot expand : Shape out of grid : " + shape);
}
public boolean isIndexValid() {
// TODO Auto-generated method stub
return true;
}
public NodeIdentifier findUniqueInstance(NodeIdentifier id) {
return store.findUniqueInstance(id);
}
public void initializeFromStorage( Storage storage ) {
//add feature types to marshaller so it'll know how to build features
Collection<FeatureType> types = store.getFeatureTypes();
for( Iterator<FeatureType> iterator = types.iterator(); iterator.hasNext(); ) {
GridData.getFeatureMarshaller().registerType((SimpleFeatureType)iterator.next());
}
//find the root node an initialize it here
ReferencedEnvelope bounds = store.getBounds();
if(bounds == null){
return; //cannot do anything because we need to know the bounds of the data.
}
this.mbr = new Region(new double[] { bounds.getMinX(), bounds.getMinY() }, new double[] { bounds.getMaxX(), bounds.getMaxY() });
this.dimension = this.mbr.getDimension();
NodeIdentifier id = findUniqueInstance(new RegionNodeIdentifier(this.mbr));
//GridRootNode tmpRootNode = new GridRootNode(gridsize, (RegionNodeIdentifier)id);
this.rootNode = null;
try{
this.rootNode = storage.get(id);
}catch (Exception ex){
//could not find root node in storage
}
if (this.rootNode == null){
this.root = null;
}else{
this.stats.reset();
this.root = this.rootNode.getIdentifier();
this.gridsize = ((GridRootNode)this.rootNode).getCapacity();
this.stats.addToDataCounter(((GridRootNode)this.rootNode).getCapacity() + 1); //children + 1 for root
this.stats.addToDataCounter(this.rootNode.getDataCount());
//here we need to match node identifies in the root.children list to the
//node identifiers in the data store
for (int i = 0; i < this.rootNode.getChildrenCount(); i ++){
RegionNodeIdentifier cid = (RegionNodeIdentifier)findUniqueInstance(this.rootNode.getChildIdentifier(i));
((GridRootNode)this.rootNode).setChildIdentifier(i, cid);
if (cid.isValid()){
Node n = readNode(cid);
this.stats.addToDataCounter(n.getDataCount());
}
}
}
}
/** Common algorithm used by both intersection and containment queries.
*
* @param type
* @param query
* @param v
*
*/
@Override
protected void rangeQuery(int type, Shape query, Visitor v) {
GridRootNode tmpRoot = (GridRootNode)this.rootNode;
//first we visit the root node
v.visitNode(tmpRoot);
if (v.isDataVisitor()){
visitData(tmpRoot, v, query, type);
}
//here we need to visit just the children that may intersect
List<Integer> childrenindex = tmpRoot.getChildren(query);
for( Iterator<Integer> iterator = childrenindex.iterator(); iterator.hasNext(); ) {
Integer childid = (Integer) iterator.next();
NodeIdentifier child = tmpRoot.getChildIdentifier(childid);
Node childNode = readNode(child);
v.visitNode(childNode);
if (v.isDataVisitor()) {
visitData(childNode, v, query, type);
}
}
}
/**
* Searches the index for missing tiles
*
* Returns both the "valid" tiles and the "invalid" tiles
*
* @param search must be within the mbr of the index
* @return
*/
public List<NodeIdentifier>[] findMissingTiles(Region search) {
List<NodeIdentifier> missing = new ArrayList<NodeIdentifier>();
List<NodeIdentifier> found = new ArrayList<NodeIdentifier>();
if (!this.root.isValid()) {
NodeCursor cc = new NodeCursor(getRootNode(), search);
NodeIdentifier next = null;
while ((next=cc.getNext()) != null){
if (!next.isValid()){
missing.add(next);
}else{
found.add(next);
}
}
}
List<NodeIdentifier>[] ret = new List[2];
ret[0] = missing;
ret[1] = found;
return ret;
}
public int getEvictions() {
return getStatistics().getEvictions();
}
public EvictionPolicy getEvictionPolicy(){
return this.policy;
}
/**
* must deal with synchronization outside this method.
*
* This will blindly evict the node.
*/
public void evict(NodeIdentifier node) {
int ret = 0;
int evictcnt = 1;
// we need to write lock the entire cache here to prevent
GridNode nodeToEvict = (GridNode) readNode(node); // FIXME: avoid to read node before
// eviction
ret = nodeToEvict.getDataCount();
// lets first evict all the children
for( int i = 0; i < nodeToEvict.getChildrenCount(); i++ ) {
Node n = readNode(nodeToEvict.getChildIdentifier(i));
ret += n.getDataCount();
n.clear();
writeNode(n);
evictcnt++;
}
// now evict the main node
nodeToEvict.clear();
writeNode(nodeToEvict);
getStatistics().addToDataCounter(-ret);
getStatistics().addToEvictionCounter(evictcnt);
}
public Node readNode(NodeIdentifier id) {
if (doRecordAccess) {
policy.access(id);
}
return super.readNode(id);
}
public GridSpatialIndexStatistics getStatistics(){
return ((GridSpatialIndexStatistics)super.getStatistics());
}
/**
* Assumes you have a write lock on the node
* you are writing.
*/
public void writeNode(Node node) {
super.writeNode(node);
if (doRecordAccess) {
policy.access(node.getIdentifier());
}
}
public boolean getDoRecordAccess(){
return this.doRecordAccess;
}
public void setDoRecordAccess(boolean b) {
doRecordAccess = b;
}
/**
* This function assumes that you have a lock on the necessary
* nodes.
*/
public void insertData(Object data, Shape shape) {
if (shape.getDimension() != dimension) {
throw new IllegalArgumentException(
"insertData: Shape has the wrong number of dimensions.");
}
if (this.root.getShape().contains(shape)) {
if (this.featureCapacity != Integer.MAX_VALUE) {
while (this.getStatistics().getNumberOfData() >= this.featureCapacity) {
if (!getEvictionPolicy().evict()) {
// no more space left and nothing else to evict
// need to evict the areas covered by this feature
NodeCursor cc = new NodeCursor(getRootNode(), shape);
NodeIdentifier next = null;
int cnt = 0;
while ((next = cc.getNext()) != null) {
Node n = readNode(next);
cnt += n.getDataCount();
n.clear();
writeNode(n);
getStatistics().addToEvictionCounter(1);
}
getStatistics().addToDataCounter(-cnt);
return;
}
}
}
insertData(this.root, data, shape);
} else {
insertDataOutOfBounds(data, shape);
}
}
}