All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.kauridev.scout.SpatialIndex Maven / Gradle / Ivy

Go to download

Scout is a simple spatial database written in Java. It is written to be used with dynamic (moving) objects in real-time.

The newest version!
/*
 * This file is part of the scout package.
 *
 * Copyright (c) 2014 Eric Fritz
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.kauridev.scout;

import com.kauridev.scout.Query.QueryResult;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

/**
 * A spatial-aware collection of elements.
 * 

* This class implements an R-tree variant designed for efficient traversal and efficient * common-case updates. *

* Note that this implementation is not synchronized. If multiple threads access the index * concurrently, and at least one of the threads modifies the index structurally, it must be * synchronized externally. A structural modification is a successful {@link #insert(Object, AABB) * insert}, {@link #update(Object, AABB) update}, or {@link #remove(Object) remove} operation. * * @author Eric Fritz */ public class SpatialIndex { /** * The minimum number of objects a node can hold before underflow. */ private final static int MIN_OBJECTS_PER_NODE = 3; /** * The maximum number of objects a node can hold before overflow. */ private final static int MAX_OBJECTS_PER_NODE = 8; /** * The maximum number of iterations of the k-means clustering algorithm to perform during node * overflow operations. */ private final static int MAX_SPLIT_ITERATIONS = 8; /** * The root node. */ private Node root; /** * An auxiliary map of objects to the leaf node where they are stored. This allows for * constant-time retrieval in the case of updates and deletions. */ private Map leafMap = new HashMap<>(); /** * The number of active traversals of the index. This field is used to forbid structural * modification operations on the index while it is being traversed, which may result in * undefined or exceptional behavior. */ private transient int traversalCount = 0; /** * Creates a new SpatialIndex. */ public SpatialIndex() { root = new Node(true); } /** * Performs a spatial query on the index, visiting all of the matching elements that satisfy the * query criteria. * * @param query The spatial query. * @param handler The handler to call for each matching element. */ public void query(Query query, QueryResultHandler handler) { traversalCount++; query(query, handler, root); traversalCount--; } /** * Performs a spatial join on the index with itself, visiting all of the matching element pairs * that satisfy the query criteria. * * @param query The spatial query. * @param handler The handler to call for each matching element. */ public void query(JoinQuery query, JoinQueryResultHandler handler) { query(this, query, handler); } /** * Performs a spatial join on this index and another spatial index, visiting all of the matching * element pairs that satisfy the query criteria. * * @param index The other spatial index. * @param query The spatial query. * @param handler The handler to call for each matching element. */ public void query(SpatialIndex index, JoinQuery query, JoinQueryResultHandler handler) { traversalCount++; this.query(query, handler, root, index.root); traversalCount--; } /** * Inserts an object into the index. *

* If volume is modified after insertion, {@link #update(Object, AABB)} should be * called in order to keep the index well-formed. * * @param object The object to insert. * @param volume The bounding volume of the object. */ @SuppressWarnings("unchecked") public void insert(E object, AABB volume) { if (traversalCount != 0) { throw new ConcurrentModificationException("Index cannot be modified during traversal."); } Node node1 = root; Node node2; while (!node1.isLeaf) { int index = chooseChildIndex(node1, volume); AABBUtil.expand(node1.volumes[index], volume); node1 = (Node) node1.entries[index]; } if (node1.numEntries + 1 <= MAX_OBJECTS_PER_NODE) { node1.add(volume, object); return; } else { node2 = splitNode(node1, volume, object); } while (node1 != root && node2 != null) { Node parent = node1.parent; Node psplit = null; updateVolumes(parent, node1); if (parent.numEntries + 1 <= MAX_OBJECTS_PER_NODE) { parent.add(getVolumeForNode(node2), node2); } else { psplit = splitNode(parent, getVolumeForNode(node2), node2); } node1 = parent; node2 = psplit; } if (node2 != null) { root = new Node(false); root.add(getVolumeForNode(node1), node1); root.add(getVolumeForNode(node2), node2); } } /** * Updates the bounds of an object in the index. If the object was not already in the index, no * action is performed. * * @param object The object to update. * @param volume The new (current) bounding volume of the object. */ public void update(E object, AABB volume) { if (traversalCount != 0) { throw new ConcurrentModificationException("Index cannot be modified during traversal."); } Node node = leafMap.get(object); if (node == null) { return; } if (node != root) { Node parent = node.parent; for (int i = 0; i < parent.numEntries; i++) { if (parent.entries[i] == node) { if (!parent.volumes[i].contains(volume)) { break; } for (int j = 0; j < node.numEntries; j++) { if (node.entries[j] == object) { node.volumes[j] = volume; return; } } } } } remove(object); insert(object, volume); } /** * Removes an object from the index. * * @param object The object to remove. If the object was not already in the index, no action is * performed. */ @SuppressWarnings("unchecked") public void remove(E object) { if (traversalCount != 0) { throw new ConcurrentModificationException("Index cannot be modified during traversal."); } Node node = leafMap.get(object); if (node == null) { return; } node.remove(object); Queue queue = new LinkedList<>(); while (node != root) { Node parent = node.parent; if (node.numEntries < MIN_OBJECTS_PER_NODE) { parent.remove(node); queue.add(node); } else { updateVolumes(parent, node); } node = parent; } if (!root.isLeaf && root.numEntries == 0) { root.isLeaf = true; } for (Node n : queue) { reinsert(n); } if (!root.isLeaf && root.numEntries == 1) { root = (Node) root.entries[0]; root.parent = null; } } @SuppressWarnings("unchecked") private boolean query(Query query, QueryResultHandler handler, Node node) { for (int i = 0; i < node.numEntries; i++) { if (node.isLeaf) { if (query.query(node.volumes[i], false) == QueryResult.PASS) { if (!handler.handle((E) node.entries[i])) { return false; } } } else { QueryResult result = query.query(node.volumes[i], true); if (result != QueryResult.FAIL) { if (result == QueryResult.PASS) { if (!visitAll(handler, (Node) node.entries[i])) { return false; } } else { if (!query(query, handler, (Node) node.entries[i])) { return false; } } } } } return true; } @SuppressWarnings("unchecked") private boolean visitAll(QueryResultHandler handler, Node node) { for (int i = 0; i < node.numEntries; i++) { if (node.isLeaf) { if (!handler.handle((E) node.entries[i])) { return false; } } else { if (!visitAll(handler, (Node) node.entries[i])) { return false; } } } return true; } @SuppressWarnings("unchecked") private boolean query(JoinQuery query, JoinQueryResultHandler handler, SpatialIndex.Node node1, SpatialIndex.Node node2) { if (node1.isLeaf && !node2.isLeaf) { for (int i = 0; i < node2.numEntries; i++) { if (!query(query, handler, node1, (SpatialIndex.Node) node2.entries[i])) { return false; } } return true; } if (node2.isLeaf && !node1.isLeaf) { for (int i = 0; i < node1.numEntries; i++) { if (!query(query, handler, (SpatialIndex.Node) node1.entries[i], node2)) { return false; } } return true; } if (!node1.isLeaf && !node2.isLeaf) { return this.queryInternal(query, handler, node1, node2); } else { return this.queryExternal(query, handler, node1, node2); } } @SuppressWarnings("unchecked") private boolean queryInternal(JoinQuery query, JoinQueryResultHandler handler, SpatialIndex.Node node1, SpatialIndex.Node node2) { for (int i = 0; i < node1.numEntries; i++) { int k = node1 == node2 ? i : 0; for (int j = k; j < node2.numEntries; j++) { if (query.query(node1.volumes[i], node2.volumes[j], true)) { if (!this.query(query, handler, (SpatialIndex.Node) node1.entries[i], (SpatialIndex.Node) node2.entries[j])) { return false; } } } } return true; } @SuppressWarnings("unchecked") private boolean queryExternal(JoinQuery query, JoinQueryResultHandler handler, SpatialIndex.Node node1, SpatialIndex.Node node2) { for (int i = 0; i < node1.numEntries; i++) { int k = node1 == node2 ? i + 1 : 0; for (int j = k; j < node2.numEntries; j++) { if (query.query(node1.volumes[i], node2.volumes[j], false)) { if (!handler.handle((E) node1.entries[i], (F) node2.entries[j])) { return false; } } else { if (node1 == node2 && !query.isSymmetric()) { if (query.query(node2.volumes[j], node1.volumes[i], false)) { if (!handler.handle((E) node2.entries[j], (F) node1.entries[i])) { return false; } } } } } } return true; } private int chooseChildIndex(Node node, AABB volume) { int index = 0; float bestArea = Float.POSITIVE_INFINITY; float bestDiff = Float.POSITIVE_INFINITY; for (int i = 0; i < node.numEntries; i++) { float area = 1; for (int j = 0; j < volume.getDimensions(); j++) { float min = Math.min(node.volumes[i].getMinimum(j), volume.getMinimum(j)); float max = Math.max(node.volumes[i].getMaximum(j), volume.getMaximum(j)); area *= max - min; } float diff = area - node.volumes[i].getVolume(); if (diff < bestDiff || (diff == bestDiff && area < bestArea)) { index = i; bestArea = area; bestDiff = diff; } } return index; } private Node splitNode(Node oldNode, AABB volume, Object object) { AABB[] volumes1 = new AABB[MAX_OBJECTS_PER_NODE]; AABB[] volumes2 = new AABB[MAX_OBJECTS_PER_NODE]; Object[] entries1 = new Object[MAX_OBJECTS_PER_NODE]; Object[] entries2 = new Object[MAX_OBJECTS_PER_NODE]; int seed1 = (int) (Math.random() * MAX_OBJECTS_PER_NODE); int seed2; do { seed2 = (int) (Math.random() * MAX_OBJECTS_PER_NODE); } while (seed1 == seed2); volumes1[0] = oldNode.volumes[seed1]; entries1[0] = oldNode.entries[seed1]; volumes2[0] = oldNode.volumes[seed2]; entries2[0] = oldNode.entries[seed2]; int size1 = 1; int size2 = 1; { double dist1 = AABBUtil.distanceSquared(volume, volumes1[0]); double dist2 = AABBUtil.distanceSquared(volume, volumes2[0]); if (dist1 < dist2) { volumes1[size1] = volume; entries1[size1] = object; size1++; } else { volumes2[size2] = volume; entries2[size2] = object; size2++; } } for (int i = 0; i < oldNode.numEntries; i++) { if (i == seed1 || i == seed2) { continue; } double dist1 = AABBUtil.distanceSquared(oldNode.volumes[i], volumes1[0]); double dist2 = AABBUtil.distanceSquared(oldNode.volumes[i], volumes2[0]); if (dist1 < dist2) { volumes1[size1] = oldNode.volumes[i]; entries1[size1] = oldNode.entries[i]; size1++; } else { volumes2[size2] = oldNode.volumes[i]; entries2[size2] = oldNode.entries[i]; size2++; } } oldNode.clear(); Node newNode = new Node(oldNode.isLeaf); partitionEntries(oldNode, newNode, volumes1, volumes2, entries1, entries2, size1, size2); return newNode; } private void partitionEntries(Node oldNode, Node newNode, AABB[] volumes1, AABB[] volumes2, Object[] entries1, Object[] entries2, int size1, int size2) { AABB median1 = volumes1[0].copy(); AABB median2 = volumes2[0].copy(); int iterations = 0; while (iterations < MAX_SPLIT_ITERATIONS) { adjustMedian(median1, volumes1, size1); adjustMedian(median2, volumes2, size2); int n = moveToGroup(volumes1, entries1, size1, volumes2, entries2, size2, median1, median2); size1 -= n; size2 += n; int m = moveToGroup(volumes2, entries2, size2, volumes1, entries1, size1, median2, median1); size1 += m; size2 -= m; if (n == 0 && m == 0) { break; } iterations++; } for (int i = 0; i < size1; i++) { oldNode.add(volumes1[i], entries1[i]); } for (int i = 0; i < size2; i++) { newNode.add(volumes2[i], entries2[i]); } } private void adjustMedian(AABB median, AABB[] volumes, int size) { float totalmass = 0; float[] centers = new float[median.getDimensions()]; for (int i = 0; i < size; i++) { float mass = volumes[i].getVolume(); for (int j = 0; j < median.getDimensions(); j++) { centers[j] += AABBUtil.getCenter(volumes[i], j) * mass; } totalmass += mass; } for (int j = 0; j < median.getDimensions(); j++) { median.setBounds(j, centers[j] / totalmass, centers[j] / totalmass); } } private int moveToGroup(AABB[] volumes1, Object[] entries1, int size1, AABB[] volumes2, Object[] entries2, int size2, AABB median1, AABB median2) { int i = 0; int transfers = 0; while (i < size1 - transfers && size1 - transfers > 1) { float dist1 = AABBUtil.distanceSquared(volumes1[i], median1); float dist2 = AABBUtil.distanceSquared(volumes1[i], median2); if (dist2 < dist1) { volumes2[size2 + transfers] = volumes1[i]; entries2[size2 + transfers] = entries1[i]; transfers++; volumes1[i] = volumes1[size1 - transfers]; entries1[i] = entries1[size1 - transfers]; } else { i++; } } return transfers; } private boolean updateVolumes(Node parent, Node node) { for (int i = 0; i < parent.numEntries; i++) { if (parent.entries[i] == node) { parent.volumes[i] = getVolumeForNode(node); return true; } } return false; } private AABB getVolumeForNode(Node node) { AABB volume = node.volumes[0].copy(); for (int i = 1; i < node.numEntries; i++) { AABBUtil.expand(volume, node.volumes[i]); } return volume; } @SuppressWarnings("unchecked") private void reinsert(Node node) { if (node.isLeaf) { for (int i = 0; i < node.numEntries; i++) { insert((E) node.entries[i], node.volumes[i]); } } else { for (int i = 0; i < node.numEntries; i++) { reinsert((Node) node.entries[i]); } } } private class Node { private Node parent; private boolean isLeaf; private int numEntries; private AABB[] volumes = new AABB[MAX_OBJECTS_PER_NODE]; private Object[] entries = new Object[MAX_OBJECTS_PER_NODE]; public Node(boolean isLeaf) { this.isLeaf = isLeaf; } @SuppressWarnings("unchecked") public void add(AABB volume, Object object) { volumes[numEntries] = volume; entries[numEntries] = object; numEntries++; if (isLeaf) { leafMap.put((E) object, this); } else { ((Node) object).parent = this; } } public void remove(Object object) { for (int i = 0; i < numEntries; i++) { if (entries[i] == object) { if (isLeaf) { leafMap.remove(object); } numEntries--; volumes[i] = volumes[numEntries]; entries[i] = entries[numEntries]; volumes[numEntries] = null; entries[numEntries] = null; } } } public void clear() { for (int i = 0; i < numEntries; i++) { if (isLeaf) { leafMap.remove(entries[i]); } volumes[i] = null; entries[i] = null; } numEntries = 0; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy