/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-2012, 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.tree.star;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.index.tree.AbstractTree;
import static org.geotoolkit.internal.tree.TreeUtilities.*;
import org.geotoolkit.index.tree.Node;
import org.geotoolkit.internal.tree.TreeAccess;
import org.geotoolkit.index.tree.StoreIndexException;
import org.geotoolkit.index.tree.TreeElementMapper;
/**
* StarRTree (R* Tree) : tree implementation.<br/><br/>
*
* It's a Tree implementation with a average duration insertion and search action.<br/>
* In the case of use, if we don't know there will be a lot of data insertion or update action forthcoming,
* this implementation is by default recommended.<br/><br/>
*
* Note : insertion action is more longer than BasicRTree, because in case of overfully Node,
* a re-insertion of all data over 33% distance near Node centroid is effectuate.<br/>
* In some case this re-insertion action permit to avoid node splitting which is expensive resource in terms.
*
* @author Remi Marechal (Geomatys).
*/
public strictfp class StarRTree<E> extends AbstractTree<E> {
/**
* In accordance with R* Tree properties.
* To avoid unnecessary split permit to
* reinsert some elements just one time.
*/
boolean insertAgain = true;
/**
* List to store data which will be insert again.
*/
private final LinkedList<Integer> listIdentifier = new LinkedList<Integer>();
private final LinkedList<double[]> listCoords = new LinkedList<double[]>();
/**
* Boolean required to stop and travel up recursively insertion action to insert select data.
*/
boolean travelUpBeforeInsertAgain = false;
/**
* Create a R* Tree implementation.
*
* @param treeAccess object in which all Tree information are stored.
* @param treeEltMap object in which data and tree identifier are stored.
* @throws StoreIndexException
* @see TreeAccess
* @see TreeElementMapper
*/
public StarRTree(final TreeAccess treeAccess, final TreeElementMapper treeEltMap) throws StoreIndexException {
super(treeAccess, treeAccess.getCRS(), treeEltMap);
ArgumentChecks.ensureNonNull("Create AbstractBasicRTree : treeAF", treeAccess);
ArgumentChecks.ensureNonNull("Create AbstractBasicRTree : CRS", crs);
super.setRoot(treeAccess.getRoot());
treeIdentifier = treeAccess.getTreeIdentifier();
}
/**
* Get statement from re-insert state.
*
* @return true if it's permit to re-insert else false.
*/
private boolean getIA() {
return insertAgain;
}
/**
* Affect statement to permit or not, re-insertion.
*
* @param insertAgain
*/
private void setIA(boolean insertAgain) {
this.insertAgain = insertAgain;
}
/**
* {@inheritDoc }.
*/
@Override
public void insert(int identifier, double... coordinates) throws IllegalArgumentException, StoreIndexException {
try {
eltCompteur++;
Node root = getRoot();
if (root == null || root.isEmpty()) {
root = createNode(null, IS_LEAF, 0, 0, 0);
root.addChild(createNode(coordinates, IS_DATA, 1, 0, -identifier));
setRoot(root);
} else {
travelUpBeforeInsertAgain = false;
final Node newRoot = nodeInsert(root, identifier, coordinates);
if (newRoot != null) {
setRoot(newRoot);
treeAccess.writeNode((Node)newRoot);
}
/**
* Insert again. Property named Tree re-balancing.
*/
if (travelUpBeforeInsertAgain) {
travelUpBeforeInsertAgain = false;
// insert again
final int siz = listCoords.size();
assert (siz == listIdentifier.size()) : "getElementAtMore33Percent : nodeInsert : lists should have same size.";
setIA(false);
final int nbrElt = getElementsNumber();
final int treeIdent = treeIdentifier; // gere quand root == null
for (int i = 0; i < siz; i++) {
if (!remove(listIdentifier.get(i), listCoords.get(i))) {
throw new AssertionError( "remove data should succeed during re-inserting event. "
+ "data identifier : "+listIdentifier.get(i)+" data boundary : "+Arrays.toString(listCoords.get(i)));
}
}
for (int i = 0; i< siz; i++) {
insert(listIdentifier.get(i), listCoords.get(i));
}
setIA(true);
assert nbrElt == getElementsNumber() : "During Insert again element number within tree should not change.";
treeIdentifier = treeIdent;
}
}
} catch (IOException ex) {
throw new StoreIndexException(this.getClass().getName()+"Tree.insert(), impossible to add element.", ex);
}
}
/**
* {@inheritDoc }.
*/
@Override
protected Node nodeInsert(Node candidate, int identifier, double... coordinates) throws IOException {
assert candidate instanceof Node;
Node fileCandidate = (Node) candidate;
assert !fileCandidate.isData() : "nodeInsert : candidate should never be data type.";
/**
* During travel down recursively candidate parent may be modified.
* When travel up recursively if candidate should be modified, get
* new candidate object updated from sub-method.
*/
Node subCandidateParent = null;
if (fileCandidate.isLeaf()) {
assert fileCandidate.checkInternal() : "nodeInsert : leaf before add.";
fileCandidate.addChild(createNode(coordinates, IS_DATA, fileCandidate.getNodeId(), 0, -((Integer)identifier)));
assert fileCandidate.checkInternal() : "nodeInsert : leaf after add.";
} else {
assert fileCandidate.checkInternal() : "nodeInsert : Node before insert.";
subCandidateParent = (Node)nodeInsert(chooseSubtree(fileCandidate, coordinates), identifier, coordinates);
add(fileCandidate.getBoundary(), coordinates);
}
/**
* Currently candidate was modified from precedently sub-Insert() call.
* Affect candidate object with new candidate from sub-Insert method.
*/
if (subCandidateParent != null) {
fileCandidate = subCandidateParent;
}
treeAccess.writeNode(fileCandidate);
if (travelUpBeforeInsertAgain) return null;
assert fileCandidate.checkInternal() : "nodeInsert : after insert.";
if (fileCandidate.getChildCount() > getMaxElements()) {
/******************************** 33 % *****************************/
if (getIA() && fileCandidate.isLeaf()) {
listIdentifier.clear();
listCoords.clear();
getElementAtMore33PerCent(candidate, listIdentifier, listCoords);
if( !listIdentifier.isEmpty()) {
travelUpBeforeInsertAgain = true;
return null;
}
}
assert fileCandidate.checkInternal() : "nodeInsert : after insert again element a 33% distance.";
/*******************************************************************/
// /*********************** Branch grafting **************************/
// if (fileCandidate.isLeaf() && fileCandidate.getParentId() != 0) {
// /**
// * We search to, travel candidate sibling from candidate parent first child, to last child.
// */
// final FileNode candidateParent = tAF.readNode(fileCandidate.getParentId());
// int sibl = candidateParent.getChildId();
// final double[] candidateBound = fileCandidate.getBoundary();
// while (sibl != 0) {
// if (sibl != fileCandidate.getNodeId()) {
// final FileNode cuSibling = tAF.readNode(sibl);
// assert cuSibling.checkInternal() : "candidate sibling.";
// if (intersects(candidateBound, cuSibling.getBoundary(), true
// && cuSibling.getChildCount() > 1)) {
// branchGrafting(fileCandidate, cuSibling);
// assert cuSibling.checkInternal() : "candidate sibling after branch grafting.";
// assert fileCandidate.checkInternal() : "candidate after branch grafting.";
// if (fileCandidate.getChildCount() <= tree.getMaxElements()) return null;
// }
// sibl = cuSibling.getSiblingId();
// } else {
// sibl = fileCandidate.getSiblingId();
// }
// }
// }
// assert fileCandidate.checkInternal() : "nodeInsert : after Branch grafting.";
// /******************************************************************/
if (fileCandidate.getChildCount() > getMaxElements()) {
// split
final Node[] splitTable = splitNode(fileCandidate);
final Node split1 = (Node)splitTable[0];
final Node split2 = (Node)splitTable[1];
final int candidateParentID = fileCandidate.getParentId();
if (candidateParentID == 0) { // on est sur le noeud root
// on clear le candidate
assert fileCandidate.getSiblingId() == 0 : "nodeInsert : split root : root should not have sibling.";
fileCandidate.clear();
fileCandidate.setProperties(IS_OTHER);
fileCandidate.addChild(split1);
fileCandidate.addChild(split2);
assert split1.checkInternal() : "nodeInsert : split1.";
assert split2.checkInternal() : "nodeInsert : split2.";
assert fileCandidate.checkInternal() : "nodeInsert : split root.";
} else {
final Node parent = treeAccess.readNode(candidateParentID);
parent.removeChild(fileCandidate);
parent.addChild(split1);
parent.addChild(split2);
assert split1.checkInternal() : "nodeInsert : split1.";
assert split2.checkInternal() : "nodeInsert : split2.";
assert parent.checkInternal() : "nodeInsert : split node.";
/**
* Candidate parent is modified, return it to re-affect appropriate parent object in up-Insert().
*/
return parent;
}
}
}
/**
* If last travel up (on Root Node) and currently candidate was changed,
* return currently Node else return null;
*/
return (subCandidateParent != null && fileCandidate.getParentId() == 0) ? fileCandidate : null;
}
/**
* Recover lesser 33% largest of {@code Node} candidate within it.
*
* @throws IllegalArgumentException if {@code Node} candidate is null.
* @return all Entry within subNodes at more 33% largest of this {@code Node}.
*/
private void getElementAtMore33PerCent(final Node candidate, LinkedList<Integer> listObjects, final LinkedList<double[]> listCoords) throws IOException {
ArgumentChecks.ensureNonNull("getElementAtMore33PerCent : candidate", candidate);
ArgumentChecks.ensureNonNull("getElementAtMore33PerCent : listObjects", listObjects);
ArgumentChecks.ensureNonNull("getElementAtMore33PerCent : listCoords", listCoords);
assert candidate.checkInternal() : "getElementAtMore33PerCent : begin candidate not conform";
final double[] canBound = candidate.getBoundary();
final double[] candidateCentroid = getMedian(canBound);
final double distPermit = calculator.getDistancePoint(getLowerCorner(canBound), getUpperCorner(canBound)) * (1/3.0);
getElementAtMore33PerCent((Node)candidate, candidateCentroid, distPermit, listObjects, listCoords);
assert candidate.checkInternal() : "getElementAtMore33PerCent : end candidate not conform";
}
/**
* Recover lesser 33% largest of {@code Node} candidate within it.
*
* @throws IllegalArgumentException if {@code Node} candidate is null.
* @return all Entry within subNodes at more 33% largest of this {@code Node}.
*/
private void getElementAtMore33PerCent(final Node candidate, double[] candidateCentroid, double distancePermit, LinkedList<Integer> listObjects, final LinkedList<double[]> listCoords) throws IOException {
ArgumentChecks.ensureNonNull("getElementAtMore33PerCent : candidateCentroid", candidateCentroid);
assert distancePermit >= 0 : "getElementsAtMore33PerCent : distancePermit. Current candidate : "+candidate;
assert candidate.checkInternal() : "getElementAtMore33PerCent : begin candidate not conform";
if (candidate.isData()) {
final double[] dataBound = candidate.getBoundary();
if (calculator.getDistancePoint(candidateCentroid, getMedian(dataBound)) > distancePermit) {
listObjects.add(-candidate.getChildId());
listCoords.add(dataBound);
}
} else {
int sibl = candidate.getChildId();
while (sibl != 0) {
final Node child = treeAccess.readNode(sibl);
getElementAtMore33PerCent(child, candidateCentroid, distancePermit, listObjects, listCoords);
sibl = child.getSiblingId();
}
assert candidate.checkInternal() : "getElementAtMore33PerCent : begin candidate not conform";
}
}
/**
* Exchange some entry(ies) between two nodes in aim to find best form with lesser overlaps.
* Also branchGrafting will be able to avoid splitting node.
*
* @param nodeA Node
* @param nodeB Node
* @throws IllegalArgumentException if nodeA or nodeB are null.
* @throws IllegalArgumentException if nodeA and nodeB have different "parent".
* @throws IllegalArgumentException if nodeA or nodeB are not tree leaf.
* @throws IllegalArgumentException if nodeA or nodeB, and their subnodes, don't contains some {@code Entry}.
*/
private void branchGrafting(final Node nodeA, final Node nodeB ) throws IllegalArgumentException, IOException {
if(!nodeA.isLeaf() || !nodeB.isLeaf()) throw new IllegalArgumentException("branchGrafting : not leaf");
assert nodeA.getParentId()== nodeB.getParentId(): "branchGrafting : NodeA and NodeB should have same parent.";
assert treeAccess.readNode(nodeA.getParentId()).checkInternal() : "branchGrafting : nodeA and B parent not conform.";
assert nodeA.checkInternal() : "branchGrafting : at begin candidate not conform";
assert nodeB.checkInternal() : "branchGrafting : at begin candidate not conform";
final int nodeACount = nodeA.getChildCount();
final int nodeBCount = nodeB.getChildCount();
final int size = nodeACount + nodeBCount;
final List<Node> listFN = new ArrayList<Node>(size);
final Node[] nodeAChildren = nodeA.getChildren();
final Node[] nodeBChildren = nodeB.getChildren();
final double[] globalE = nodeAChildren[0].getBoundary().clone();
for (Node nod : nodeAChildren) {
add(globalE, nod.getBoundary());
listFN.add(nod);
}
for (Node nod : nodeBChildren) {
add(globalE, nod.getBoundary());
listFN.add(nod);
}
if(listFN.isEmpty()) throw new IllegalArgumentException("branchGrafting : empty list");
final int maxEltsPermit = getMaxElements();
final int dim = globalE.length >> 1;
double lengthDimRef = -1;
int indexSplit = -1;
//split along the longest span
for(int i = 0; i < dim; i++) {
double lengthDimTemp = getSpan(globalE, i);
if (lengthDimTemp > lengthDimRef) {
lengthDimRef = lengthDimTemp;
indexSplit = i;
}
}
assert indexSplit != -1 : "BranchGrafting : indexSplit not find"+indexSplit;
calculator.sortList(indexSplit, true, listFN);
double[] envB, envA;
double overLapsRef = Double.POSITIVE_INFINITY;
int index = -1;
final int size04 = (int) Math.max(size * 0.4, 1);
for (int cut = size04; cut < size-size04; cut++) {
envA = listFN.get(0).getBoundary().clone();
for (int i = 1; i<cut; i++) {
add(envA, listFN.get(i).getBoundary());
}
envB = listFN.get(cut).getBoundary().clone();
for (int i = cut + 1; i < size; i++) {
add(envB, listFN.get(i).getBoundary());
}
double overLapsTemp = calculator.getOverlaps(envA, envB);
if (overLapsTemp < overLapsRef) {
overLapsRef = overLapsTemp;
index = cut;
}
}
//index not wrong a split is better.
if (index > maxEltsPermit || (size-index) > maxEltsPermit) return;
nodeA.clear();
nodeB.clear();
for (int i = 0; i < index; i++) {
final Node newChild = (Node)listFN.get(i);
newChild.setSiblingId(0);
nodeA.addChild(newChild);
}
for (int i = index; i < size; i++) {
final Node newChild = (Node)listFN.get(i);
newChild.setSiblingId(0);
nodeB.addChild(newChild);
}
assert nodeA.getParentId()== nodeB.getParentId() : "branchGrafting : NodeA and NodeB should have same parent.";
assert treeAccess.readNode(nodeA.getParentId()).checkInternal() : "branchGrafting : nodeA and B parent not conform.";
assert nodeA.checkInternal() : "branchGrafting : at end candidate not conform";
assert nodeB.checkInternal() : "branchGrafting : at end candidate not conform";
}
/**
* Condense R-Tree.
*
* Condense made, travel up from leaf to tree trunk.
*
* @param candidate {@code Node} to begin condense.
* @throws IllegalArgumentException if candidate is null.
*/
@Override
protected void trim(final Node candidate) throws IllegalArgumentException, IOException, StoreIndexException {
ArgumentChecks.ensureNonNull("trim : Node candidate", candidate);
List<double[]> reinsertListCoords = null;
List<Integer> reinsertListObjects = null;
double[] candiBound = null;
if (candidate.getChildId() != 0 && !candidate.isLeaf()) {
int sibl = candidate.getChildId();
while (sibl != 0) {
final Node currentChild = treeAccess.readNode(sibl);
// empty child
if (currentChild.isEmpty()) {
candidate.removeChild(currentChild);
} else if (currentChild.isLeaf()
&& currentChild.getChildCount() <= getMaxElements() / 3) {// other condition
if (reinsertListCoords == null) {
reinsertListCoords = new ArrayList<double[]>();
reinsertListObjects = new ArrayList<Integer>();
}
int cuChildSibl = currentChild.getChildId();
while (cuChildSibl != 0) {
final Node currentData = treeAccess.readNode(cuChildSibl);
reinsertListCoords.add(currentData.getBoundary());// risk de .clone a voir
reinsertListObjects.add(-currentData.getChildId());
cuChildSibl = currentData.getSiblingId();
currentChild.removeChild(currentData);
setElementsNumber(getElementsNumber()-1);
}
candidate.removeChild(currentChild);
} else {
if (candiBound == null) {
candiBound = currentChild.getBoundary().clone();
} else {
add(candiBound, currentChild.getBoundary());
}
// child own a single sub-child and its not a leaf.
if (currentChild.getChildCount() == 1) {
assert !currentChild.isLeaf() : "Trim : current child should not be leaf.";
final Node cChild = treeAccess.readNode(currentChild.getChildId());
assert Arrays.equals(currentChild.getBoundary(), cChild.getBoundary()) : "Node with only one element should have same boundary than its stored element.";
candidate.removeChild(currentChild);
candidate.addChild(cChild);
}
}
sibl = currentChild.getSiblingId();
}
}
if (candiBound != null) {
candidate.setBoundary(candiBound);
treeAccess.writeNode(candidate);
assert candidate.checkInternal() : "trim : candidate not conform";
}
if (candidate.getParentId()!= 0) {
trim (treeAccess.readNode(candidate.getParentId()));
} else {
// generaly root have some changes.
if (candidate.isEmpty()) {
setRoot(null);
} else {
setRoot(candidate);
}
}
if (reinsertListCoords != null) {
assert (reinsertListObjects != null) : "trim : listObjects should not be null.";
final int reSize = reinsertListCoords.size();
assert (reSize == reinsertListObjects.size()) : "reinsertLists should have same size";
for (int i = 0; i < reSize; i++) {
insert(reinsertListObjects.get(i), reinsertListCoords.get(i));
}
} else {
assert (reinsertListObjects == null) : "trim : listObjects should be null.";
}
}
}