/* * 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; import java.io.IOException; import java.util.Arrays; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.Classes; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.geotoolkit.internal.tree.TreeUtilities; import org.geotoolkit.internal.tree.CalculatorND; import org.geotoolkit.internal.tree.Calculator; import org.geotoolkit.internal.tree.TreeAccess; import org.apache.sis.util.Utilities; import static org.geotoolkit.internal.tree.TreeUtilities.*; /** * Create an abstract Tree. * * @author RĂ©mi Marechal (Geomatys). * @author Martin Desruisseaux (Geomatys). */ public abstract class AbstractTree<E> implements Tree<E> { /** * Object which store Tree informations in memory or on hard disk. */ protected final TreeAccess treeAccess; /** * Maximum element authorized per Node. */ private final int maxElementPerNode; /** * data {@link CoordinateReferenceSystem}. */ protected final CoordinateReferenceSystem crs; /** * Object which effectuate compute needed by tree to store datas. */ protected final Calculator calculator; /** * Object which link data and Tree identifier. */ private final TreeElementMapper<E> treeEltMap; /** * identifier use by tree which is linked at a data.<br/> * Each identifier is link on only one distinct data. */ protected int treeIdentifier; /** * Data number stored in this Tree. */ protected int eltCompteur; /** * Tree trunk. Root Node. */ private Node root; /** * Tree fundation implementation. * * @param treeAccess to store Tree informations in memory or on hard disk. * @param crs data {@link CoordinateReferenceSystem}. * @param treeEltMap to link data and Tree identifier. */ protected AbstractTree(final TreeAccess treeAccess, final CoordinateReferenceSystem crs, final TreeElementMapper<E> treeEltMap) { ArgumentChecks.ensureNonNull("Create Tree : CRS", crs); ArgumentChecks.ensureNonNull("Create TreeAccess : treeAccess", treeAccess); ArgumentChecks.ensureNonNull("Create TreeElementMapper : treeEltMap", treeEltMap); this.maxElementPerNode = treeAccess.getMaxElementPerCells(); ArgumentChecks.ensureBetween("Create Tree : maxElements", 2, Integer.MAX_VALUE, maxElementPerNode); this.treeAccess = treeAccess; this.treeEltMap = treeEltMap; this.calculator = new CalculatorND(); this.eltCompteur = treeAccess.getEltNumber(); this.crs = crs; } /** * {@inheritDoc} */ @Override public synchronized int[] searchID(final Envelope regionSearch) throws StoreIndexException { ArgumentChecks.ensureNonNull("Envelope regionSearch", regionSearch); final Node root = getRoot(); final double[] regSearch = TreeUtilities.getCoords(regionSearch); if (root != null && !root.isEmpty()) { try { return treeAccess.search(root.getNodeId(), regSearch); } catch (IOException ex) { throw new StoreIndexException(this.getClass().getName()+" impossible to find stored elements at " +Arrays.toString(regSearch)+" region search area.", ex); } } return new int[0]; } /** * {@inheritDoc }. */ @Override public TreeIdentifierIterator search(final Envelope regionSearch) throws StoreIndexException { ArgumentChecks.ensureNonNull("Envelope regionSearch", regionSearch); final double[] regSearch = TreeUtilities.getCoords(regionSearch); return new TreeIntegerIdentifierIterator(treeAccess, regSearch); } /** * {@inheritDoc} */ @Override public synchronized int insert(final E object) throws IllegalArgumentException , StoreIndexException{ try { ArgumentChecks.ensureNonNull("insert : object", object); final Envelope env = treeEltMap.getEnvelope(object); if (!Utilities.equalsIgnoreMetadata(crs, env.getCoordinateReferenceSystem())) throw new IllegalArgumentException("During insertion element should have same CoordinateReferenceSystem as Tree."); final double[] coordinates = TreeUtilities.getCoords(env); for (double d : coordinates) if (Double.isNaN(d)) throw new IllegalArgumentException("coordinates contain at least one NAN value"); treeEltMap.setTreeIdentifier(object, treeIdentifier); insert(treeIdentifier, coordinates); treeIdentifier++; return treeIdentifier - 1; } catch (IOException ex) { throw new StoreIndexException(ex); } } /** * {@inheritDoc} */ public void insert(final int identifier, final 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 { final Node newRoot = nodeInsert(root, identifier, coordinates); if (newRoot != null) { setRoot(newRoot); treeAccess.writeNode((Node)newRoot); } } } catch (IOException ex) { throw new StoreIndexException(this.getClass().getName()+"Tree.insert(), impossible to add element.", ex); } } /** * Insert data in the current {@link Node}.<br/><br/> * * In some case, parent candidate Node is modify and it is returned during recursively travel up. * * @param candidate current Node in travel down insertion. * @param identifier data tree identifier. * @param coordinates data boundary * @return parent candidate Node. * @throws IOException if problem during hard drive writing. */ protected abstract Node nodeInsert(final Node candidate, final int identifier, final double ...coordinates) throws IOException; /** * Find appropriate {@code Node} to insert data.<br/><br/> * To define appropriate Node, criterion are :<br/> * - require minimum area enlargement to cover shape.<br/> * - or put into {@code Node} with lesser elements number in case of area equals. * * @param children List of {@code Node}. * @param entry {@code Envelope} to add. * @throws IllegalArgumentException if children or entry are null. * @throws IllegalArgumentException if children is empty. * @return {@code Node} which is appropriate to contain shape. */ protected Node chooseSubtree(final Node candidate, final double... coordinates) throws IOException { ArgumentChecks.ensureNonNull("chooseSubtree : candidate", candidate); ArgumentChecks.ensureNonNull("chooseSubtree : coordinates", coordinates); final int childCount = candidate.getChildCount(); if (childCount == 0) throw new IllegalArgumentException("chooseSubtree : children is empty"); if (childCount == 1) return treeAccess.readNode(candidate.getChildId()); final Node[] children = candidate.getChildren(); assert children.length == childCount : "choose subtree : childcount should have same length as children table."; Node containNode = null; int cNElt = Integer.MAX_VALUE; for (final Node fNod : children) { assert fNod.checkInternal() : "chooseSubTree : test contains."; if (contains(fNod.getBoundary(), coordinates, true)) { final int chCount = fNod.getChildCount(); if (chCount < cNElt) { containNode = fNod; cNElt = chCount; } } } if (containNode != null) return containNode; Node result = children[0]; double[] addBound = result.getBoundary().clone(); for(int i = 1; i < childCount; i++) { add(addBound, children[i].getBoundary()); } double area = calculator.getSpace(addBound); double nbElmt = result.getChildCount(); double areaTemp; for (int i = 0; i < childCount; i++) { final Node dn = children[i]; assert dn.checkInternal() : "chooseSubtree : find subtree."; final double[] dnBoundary = dn.getBoundary(); final double[] rnod = dnBoundary.clone(); add(rnod, coordinates); final int nbe = dn.getChildCount(); final double[] assertBound = dnBoundary.clone(); areaTemp = calculator.getEnlargement(dnBoundary, rnod); assert Arrays.equals(dnBoundary, assertBound); if (areaTemp < area) { result = dn; area = areaTemp; nbElmt = nbe; } else if (areaTemp == area) { if (nbe < nbElmt) { result = dn; area = areaTemp; nbElmt = nbe; } } } return result; } /** * Split a overflow {@code Node} in accordance with R-Tree properties. * * @param candidate {@code Node} to Split. * @throws IllegalArgumentException if candidate is null. * @throws IllegalArgumentException if candidate elements number is lesser 2. * @return {@code Node} List which contains two {@code Node} (split result of candidate). */ protected Node[] splitNode(final Node candidate) throws IllegalArgumentException, IOException { ArgumentChecks.ensureNonNull("splitNode : candidate", candidate); assert candidate.checkInternal() : "splitNode : begin."; final Node[] children = candidate.getChildren(); final byte candidateProperties = candidate.getProperties(); final int splitIndex = defineSplitAxis(children); final int size = children.length; final double size04 = size * 0.4; final int demiSize = (int) ((size04 >= 1) ? size04 : 1); double[] unionTabA, unionTabB; // find a solution where overlaps between 2 groups is the smallest. double bulkTemp; double bulkRef = Double.POSITIVE_INFINITY; // in case where overlaps equal 0 (no overlaps) find the smallest group area. double areaTemp; double areaRef = Double.POSITIVE_INFINITY; // solution int index = 0; boolean lower_or_upper = true; int cut2; //compute with lower and after upper for(int lu = 0; lu < 2; lu++) { calculator.sort(splitIndex, (lu == 0), children); for(int cut = demiSize; cut <= size - demiSize; cut++) { cut2 = size - cut; final Node[] splitTabA = new Node[cut]; final Node[] splitTabB = new Node[cut2]; System.arraycopy(children, 0, splitTabA, 0, cut); System.arraycopy(children, cut, splitTabB, 0, cut2); // bulk computing unionTabA = splitTabA[0].getBoundary().clone(); for (int i = 1; i < cut; i++) { add(unionTabA, splitTabA[i].getBoundary()); } unionTabB = splitTabB[0].getBoundary().clone(); for (int i = 1; i < cut2; i++) { add(unionTabB, splitTabB[i].getBoundary()); } bulkTemp = calculator.getOverlaps(unionTabA, unionTabB); if (bulkTemp == 0) { areaTemp = calculator.getEdge(unionTabA) + calculator.getEdge(unionTabB); if (areaTemp < areaRef) { areaRef = areaTemp; index = cut; lower_or_upper = (lu == 0); } } else { if (!Double.isInfinite(areaRef)) continue; // a better solution was already found. if (bulkTemp < bulkRef) { bulkRef = bulkTemp; index = cut; lower_or_upper = (lu == 0); } } } } // best organization solution calculator.sort(splitIndex, lower_or_upper, children); final int lengthResult2 = size - index; final Node[] result1Children = new Node[index]; final Node[] result2Children = new Node[lengthResult2]; final Node result1, result2; final boolean isLeaf = candidate.isLeaf(); if (!isLeaf && index == 1) { result1 = children[0]; ((Node)result1).setSiblingId(0); } else { result1 = createNode(null, candidateProperties, 0, 0, 0); System.arraycopy(children, 0, result1Children, 0, index); result1.addChildren(result1Children); } if (!isLeaf && lengthResult2 == 1) { result2 = children[size-1]; ((Node)result2).setSiblingId(0); } else { result2 = createNode(null, candidateProperties, 0, 0, 0); System.arraycopy(children, index, result2Children, 0, lengthResult2); result2.addChildren(result2Children); } // check result assert result1.checkInternal() : "splitNode : result1."; assert result2.checkInternal() : "splitNode : result2."; return new Node[]{result1, result2}; } /** * Compute and define which axis to split {@code Node} candidate. * * <blockquote><font size=-1> * <strong>NOTE: Define split axis method decides a split axis among all dimensions. * The chosen axis is the one with smallest overall perimeter or area (in function with dimension size). * It work by sorting all entry or {@code Node}, from their left boundary coordinates. * Then it considers every divisions of the sorted list that ensure each node is at least 40% full. * The algorithm compute perimeter or area of two result {@code Node} from every division. * A second pass repeat this process with respect their right boundary coordinates. * Finally the overall perimeter or area on one axis is the sum of all perimeter or area obtained from the two pass.</strong> * </font></blockquote> * * @throws IllegalArgumentException if candidate is null. * @return prefered ordinate index to split. */ protected int defineSplitAxis(final Node[] children) throws IOException { ArgumentChecks.ensureNonNull("candidate : ", children); final int size = children.length; final double[][] childsBound = new double[size][]; int cbID = 0; for (Node nod : children) childsBound[cbID++] = nod.getBoundary(); final double size04 = size * 0.4; final int demiSize = (int) ((size04 >= 1) ? size04 : 1); double[][] splitTabA, splitTabB; double[] gESPLA, gESPLB; double bulkTemp; double bulkRef = Double.POSITIVE_INFINITY; int index = 0; final double[] globalEltsArea = getEnvelopeMin(childsBound); final int dim = globalEltsArea.length >> 1; // if glogaleArea.span(currentDim) == 0 || if all elements have same span // value as global area on current ordinate, impossible to split on this axis. unappropriateOrdinate : for (int indOrg = 0; indOrg < dim; indOrg++) { final double globalSpan = getSpan(globalEltsArea, indOrg); boolean isSameSpan = true; //check if its possible to split on this currently ordinate. for (double[] elt : childsBound) { if (!(Math.abs(getSpan(elt, indOrg) - globalSpan) <= 1E-9)) { isSameSpan = false; break; } } if (globalSpan <= 1E-9 || isSameSpan) continue unappropriateOrdinate; bulkTemp = 0; for (int left_or_right = 0; left_or_right < 2; left_or_right++) { calculator.sort(indOrg, left_or_right == 0, childsBound); for (int cut = demiSize, sdem = size - demiSize; cut <= sdem; cut++) { splitTabA = new double[cut][]; splitTabB = new double[size - cut][]; System.arraycopy(childsBound, 0, splitTabA, 0, cut); System.arraycopy(childsBound, cut, splitTabB, 0, size-cut); gESPLA = getEnvelopeMin(splitTabA); gESPLB = getEnvelopeMin(splitTabB); bulkTemp += calculator.getEdge(gESPLA); bulkTemp += calculator.getEdge(gESPLB); } } if(bulkTemp < bulkRef) { bulkRef = bulkTemp; index = indOrg; } } return index; } /** * {@inheritDoc }. */ @Override public synchronized boolean remove(final E object) throws StoreIndexException { try { ArgumentChecks.ensureNonNull("Object to remove", object); final Envelope env = treeEltMap.getEnvelope(object); final int entry = treeEltMap.getTreeIdentifier(object); return remove(entry, env); } catch (IOException ex) { throw new StoreIndexException(ex); } } public boolean remove(final int entry, Envelope entryEnvelope) throws StoreIndexException { ArgumentChecks.ensureNonNull("Envelope for the entry to remove", entryEnvelope); if (!Utilities.equalsIgnoreMetadata(crs, entryEnvelope.getCoordinateReferenceSystem())) throw new IllegalArgumentException("During insertion element should have same CoordinateReferenceSystem as Tree."); final double[] coordinates = TreeUtilities.getCoords(entryEnvelope); for (double d : coordinates) if (Double.isNaN(d)) throw new IllegalArgumentException("coordinates contain at least one NAN value"); return remove(entry, coordinates); } /** * Remove data. * * @param identifier data tree identifier. * @param coordinates data boundary * @return true if data have been correctively removed else false. * @throws IOException if problem during hard drive writing. */ protected boolean remove(final int identifier, final double... coordinates) throws StoreIndexException { ArgumentChecks.ensureNonNull("remove : object", identifier); ArgumentChecks.ensureNonNull("remove : coordinates", coordinates); final Node root = getRoot(); if (root != null) { try { final boolean removed = removeNode(root, identifier, coordinates); return removed; } catch (IOException ex) { throw new StoreIndexException(this.getClass().getName() +"impossible to remove object : "+identifier +" at coordinates : "+Arrays.toString(coordinates), ex); } } return false; } /** * Travel {@code Tree}, find data if it exist and remove it. * * <blockquote><font size=-1> * <strong>NOTE: Moreover {@code Tree} is condensate after a deletion to stay conform about R-Tree properties.</strong> * </font></blockquote> * * @param identifier data tree identifier. * @param coordinates data boundary * @throws IllegalArgumentException if candidate or entry is null. * @return true if entry is found and removed else false. */ protected boolean removeNode(final Node candidate, final int identifier, final double... coordinate) throws IllegalArgumentException, StoreIndexException, IOException{ ArgumentChecks.ensureNonNull("removeNode : Node candidate", candidate); ArgumentChecks.ensureNonNull("removeNode : Object object", identifier); ArgumentChecks.ensureNonNull("removeNode : double[] coordinate", coordinate); if(intersects(candidate.getBoundary(), coordinate, true)){ if (candidate.isLeaf()) { boolean removed = candidate.removeData(identifier, coordinate); if (removed) { setElementsNumber(getElementsNumber()-1); trim(candidate); return true; } } else { int sibl = candidate.getChildId(); while (sibl != 0) { final Node currentChild = treeAccess.readNode(sibl); final boolean removed = removeNode(currentChild, identifier, coordinate); if (removed) return true; sibl = currentChild.getSiblingId(); } } } return false; } /** * Condense R-Tree. * * Condense made, travel up from leaf to tree trunk (root Node). * * @param candidate {@code Node} to begin condense. * @throws IllegalArgumentException if candidate is null. */ protected abstract void trim(final Node candidate) throws IllegalArgumentException, IOException, StoreIndexException ; /** * {@inheritDoc} */ @Override public int getMaxElements() { return this.maxElementPerNode; } /** * {@inheritDoc} */ @Override public Node getRoot() { return this.root; } /** * {@inheritDoc} */ @Override public void setRoot(final Node root) throws StoreIndexException{ this.root = root; if (root == null) { try { treeAccess.rewind(); } catch (IOException ex) { throw new StoreIndexException("Impossible to rewind treeAccess during setRoot(null).", ex); } treeIdentifier = 1; eltCompteur = 0; } } /** * {@inheritDoc} */ @Override public CoordinateReferenceSystem getCrs(){ return crs; } /** * {@inheritDoc} */ @Override public synchronized void clear() throws StoreIndexException { setRoot(null); } /** * {@inheritDoc} */ @Override public int getElementsNumber() { return eltCompteur; } /** * {@inheritDoc} */ public void setElementsNumber(final int value) { this.eltCompteur = value; } /** * {@inheritDoc }. */ @Override public TreeElementMapper getTreeElementMapper() { return treeEltMap; } /** * {@inheritDoc }. */ @Override public void close() throws IOException { treeAccess.setTreeIdentifier(treeIdentifier); treeAccess.setEltNumber(eltCompteur); treeAccess.close(); treeEltMap.close(); } /** * {@inheritDoc }. */ @Override public synchronized void flush() throws StoreIndexException { try { treeAccess.setTreeIdentifier(treeIdentifier); treeAccess.setEltNumber(eltCompteur); treeAccess.flush(); treeEltMap.flush(); } catch (IOException ex) { throw new StoreIndexException("FileBasicRTree : close(). Impossible to close TreeAccessFile.", ex); } } /** * {@inheritDoc } */ @Override public boolean isClosed() { return treeAccess.isClose(); } TreeAccess getTreeAccess() { return treeAccess; } /** * {@inheritDoc} */ @Override public double[] getExtent() throws StoreIndexException { final Node node = getRoot(); return (node == null) ? null : node.getBoundary().clone(); } /** * {@inheritDoc} */ @Override public String toString() { final Node root = getRoot(); final String strRoot = (root == null || root.isEmpty()) ?"null":root.toString(); return Classes.getShortClassName(this) + "\n" + strRoot; } /** * Create a {@link Node} in accordance with RTree properties. * * @param tree pointer on Tree. * @param parent pointer on parent {@code Node}. * @param children sub {@code Node}. * @param entries entries {@code List} to add in this node. * @param coordinates lower upper bounding box coordinates table. * @return appropriate Node from tree. */ protected Node createNode(final double[] boundary, final byte properties, final int parentId, final int siblingId, final int childId) throws IllegalArgumentException { return treeAccess.createNode(boundary, properties, parentId, siblingId, childId); } }