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

edu.uci.ics.jung.visualization.spatial.SpatialQuadTree Maven / Gradle / Ivy

package edu.uci.ics.jung.visualization.spatial;

import static edu.uci.ics.jung.visualization.spatial.SpatialQuadTree.Quadrant.*;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import edu.uci.ics.jung.layout.model.LayoutModel;
import edu.uci.ics.jung.layout.model.Point;
import edu.uci.ics.jung.layout.util.LayoutChangeListener;
import edu.uci.ics.jung.layout.util.LayoutEvent;
import edu.uci.ics.jung.layout.util.LayoutNetworkEvent;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A spatial data structure that uses a quadtree.
 *
 * @author Tom Nelson
 * @param  the node type
 */
public class SpatialQuadTree extends AbstractSpatial
    implements TreeNode, Spatial, LayoutChangeListener {

  private static final Logger log = LoggerFactory.getLogger(SpatialQuadTree.class);

  private final Object lock = new Object();

  @Override
  public Rectangle2D getBounds() {
    return rectangle;
  }

  @Override
  public Collection getChildren() {
    return children.values();
  }

  /** the four quadrant keys for the child cells */
  enum Quadrant {
    NE,
    NW,
    SW,
    SE;
  }

  /** how many nodes per cell */
  private int MAX_OBJECTS = 1;
  /** max tree height */
  private int MAX_LEVELS = 12;

  /** the level of this cell in the tree */
  private int level;
  /** the nodes contains in this cell, assuming this cell is a leaf */
  private Set nodes;
  /** the area for this cell */
  private Rectangle2D area;
  /** a collection of child nodes, assuming this is not a leaf */
  private Map> children;

  /** a cache of grid cell rectangles for performance */
  private List gridCache;

  //  private Collection pickShapes = EvictingQueue.create(4);

  /** @param layoutModel */
  public SpatialQuadTree(LayoutModel layoutModel) {
    this(layoutModel, 0, 0, 0, layoutModel.getWidth(), layoutModel.getHeight());
  }

  /**
   * @param layoutModel
   * @param width
   * @param height
   */
  public SpatialQuadTree(LayoutModel layoutModel, double width, double height) {
    this(layoutModel, 0, 0, 0, width, height);
  }

  /**
   * @param level level to start at. 0 is the root
   * @param x
   * @param y
   * @param width
   * @param height
   */
  public SpatialQuadTree(
      LayoutModel layoutModel, int level, double x, double y, double width, double height) {
    this(layoutModel, level, new Rectangle2D.Double(x, y, width, height));
  }

  public SpatialQuadTree(LayoutModel layoutModel, int pLevel, Rectangle2D area) {
    super(layoutModel);
    level = pLevel;
    nodes = Collections.synchronizedSet(Sets.newHashSet());
    this.area = area;
  }

  /**
   * @param o max number of objects allowed
   * @return this QuadTree
   */
  public SpatialQuadTree setMaxObjects(int o) {
    MAX_OBJECTS = o;
    return this;
  }

  /**
   * @param l max levels allowed
   * @return
   */
  public SpatialQuadTree setMaxLevels(int l) {
    MAX_LEVELS = l;
    return this;
  }

  /** @return the level of this cell */
  protected int getLevel() {
    return level;
  }

  /** @return the nodes in this cell, assuming it is a leaf */
  public Set getNodes() {
    return nodes;
  }

  /*
   * Clears the quadtree
   */
  @Override
  public void clear() {
    nodes.clear();
    synchronized (lock) {
      children = null;
      gridCache = null;
    }
  }

  /*
   * Splits the Quadtree into 4 sub-QuadTrees
   */
  protected void split() {
    log.trace("splitting {}", this);
    double width = (area.getWidth() / 2);
    double height = (area.getHeight() / 2);
    double x = area.getX();
    double y = area.getY();

    int childLevel = level + 1;
    SpatialQuadTree ne =
        new SpatialQuadTree(layoutModel, childLevel, x + width, y, width, height);
    SpatialQuadTree nw = new SpatialQuadTree(layoutModel, childLevel, x, y, width, height);
    SpatialQuadTree sw =
        new SpatialQuadTree(layoutModel, childLevel, x, y + height, width, height);
    SpatialQuadTree se =
        new SpatialQuadTree(layoutModel, childLevel, x + width, y + height, width, height);
    synchronized (lock) {
      children = ImmutableMap.of(NE, ne, NW, nw, SW, sw, SE, se);
    }
  }

  /**
   * find the quadrant that the point would be in
   *
   * @param p the point of interest
   * @return the quadrant that would contain the point
   */
  protected Quadrant getQuadrant(Point p) {
    return getQuadrant(p.x, p.y);
  }

  /**
   * find the quadrant that the point would be in
   *
   * @param x, y the point of interest
   * @return the quadrant that would contain the point
   */
  protected Quadrant getQuadrant(double x, double y) {

    double centerX = area.getCenterX();
    double centerY = area.getCenterY();

    boolean inNorth = y < centerY;

    boolean inSouth = y >= centerY;

    boolean inWest = x < centerX;

    if (inNorth && inWest) {
      return Quadrant.NW;
    }
    if (inSouth && inWest) {
      return Quadrant.SW;
    }
    boolean inEast = x >= centerX;
    if (inNorth && inEast) {
      return Quadrant.NE;
    }
    if (inSouth && inEast) {
      return Quadrant.SE;
    }
    return null;
  }

  /*
   * Insert the object into the quadtree. If the node exceeds the capacity, it
   * will split and add all objects to their corresponding nodes.
   */
  protected void insert(N p) {
    gridCache = null;
    log.trace("{} inserting {} at {}", this, p, layoutModel.apply(p));
    if (children != null) {
      // there are child QuadTrees available
      Quadrant quadrant = getQuadrant(layoutModel.apply(p));
      if (quadrant != null && children.get(quadrant) != null) {
        // insert into the child QuadTree
        children.get(quadrant).insert(p);
        return;
      }
    }
    // insert into this QuadTree unless capacity is exceeded
    nodes.add(p);
    // if capacity is exceeded, split and put all objects into child QuadTrees
    if (nodes.size() > MAX_OBJECTS && level < MAX_LEVELS) {
      split();
      // now this QuadTree has child QuadTrees

      for (Iterator iterator = nodes.iterator(); iterator.hasNext(); ) {
        N node = iterator.next();
        Quadrant quadrant = getQuadrant(layoutModel.apply(node));
        children.get(quadrant).insert(node);
        iterator.remove();
      }
    }
  }

  /*
   * Return all objects that are within the passed rectangle
   */
  protected Set retrieve(Set returnObjects, Rectangle2D r) {
    if (children == null) {
      // i am a leaf, add any nodes i have
      returnObjects.addAll(nodes);
    } else {

      for (Map.Entry> entry : children.entrySet()) {
        if (entry.getValue().area.intersects(r)) {
          children.get(entry.getKey()).retrieve(returnObjects, r);
        }
      }
    }
    return returnObjects;
  }

  /**
   * Return all objects that are within the passed shape This is needed when the layout is
   * rotated/skewed and the shape edges are no longer parallel to the grid edges.
   */
  protected Set retrieve(Set returnObjects, Shape shape) {
    if (children == null) {
      // i am a leaf, add any nodes i have
      returnObjects.addAll(nodes);
    } else {

      synchronized (lock) {
        for (Map.Entry> entry : children.entrySet()) {
          if (shape.intersects(entry.getValue().area)) {
            children.get(entry.getKey()).retrieve(returnObjects, shape);
          }
        }
      }
    }
    return returnObjects;
  }

  public List getNodes(List list) {
    if (gridCache == null) {
      list.addAll(this.collectNodes(list, this));
      gridCache = list;
    }
    return gridCache;
  }

  @Override
  public List getGrid() {
    List areas = Lists.newArrayList();

    return collectGrids(areas, this);
  }

  private List collectGrids(List list, SpatialQuadTree tree) {
    list.add(tree.area);
    if (tree.children != null) {
      for (Map.Entry> entry : tree.children.entrySet()) {
        collectGrids(list, entry.getValue());
      }
    }
    return list;
  }

  private List collectNodes(List list, SpatialQuadTree tree) {
    list.add(tree);
    if (tree.children != null) {
      for (Map.Entry> entry : tree.children.entrySet()) {
        collectNodes(list, entry.getValue());
      }
    }
    return list;
  }

  /**
   * @param shape the possibly non-rectangular area of interest
   * @return the nodes that are in the quadtree cells that intersect with the passed shape
   */
  @Override
  public Set getVisibleElements(Shape shape) {
    if (!isActive()) {
      log.trace("not active so getting from the graph");
      return layoutModel.getGraph().nodes();
    }

    pickShapes.add(shape);
    Set set = Sets.newHashSet();
    Set visibleNodes = this.retrieve(set, shape);
    if (log.isDebugEnabled()) {
      log.debug("visibleNodes:{}", visibleNodes);
    }

    return visibleNodes;
  }

  /**
   * @param r
   * @return the nodes that are in the quadtree cells that intersect with the passed rectangle
   */
  public Set getVisibleNodes(Rectangle2D r) {
    if (!isActive()) {
      log.trace("not active so getting from the graph");
      return layoutModel.getGraph().nodes();
    }

    Set set = Sets.newHashSet();
    Set visibleNodes = this.retrieve(set, r);
    if (log.isDebugEnabled()) {
      log.debug("visibleNodes:{}", visibleNodes);
    }
    return visibleNodes;
  }

  /**
   * tha layout area that this tree cell operates over
   *
   * @return
   */
  @Override
  public Rectangle2D getLayoutArea() {
    return area;
  }

  @Override
  public void recalculate() {
    if (isActive()) {
      recalculate(layoutModel.getGraph().nodes());
    }
  }

  private void recalculate(Collection nodes) {

    this.clear();
    while (true) {
      try {
        for (N node : nodes) {
          this.insert(node);
        }
        break;
      } catch (ConcurrentModificationException ex) {
        // ignore
      }
    }
  }

  /**
   * @param node the node to search for
   * @return the quadtree leaf that contains the passed node
   */
  public TreeNode getContainingQuadTreeLeaf(N node) {
    // find where it is now, not where the layoutModel will put it
    if (this.nodes.contains(node)) {
      if (log.isTraceEnabled()) {
        log.trace("nodes {} in {} does contain {}", nodes, this, node);
      }
      return this;
    }
    if (children != null) {
      for (Map.Entry> entry : children.entrySet()) {
        SpatialQuadTree child = entry.getValue();
        TreeNode leaf = child.getContainingQuadTreeLeaf(node);
        if (leaf != null) {
          return leaf;
        }
      }
    }
    return null;
  }

  public Set> getContainingLeafs(Point2D p) {
    return Collections.singleton(getContainingQuadTreeLeaf(p));
  }

  public Set> getContainingLeafs(double x, double y) {
    return Collections.singleton(getContainingQuadTreeLeaf(x, y));
  }

  @Override
  public TreeNode getContainingLeaf(Object element) {

    return getContainingQuadTreeLeaf((N) element);
  }

  /**
   * find the cell that would contain the passed point
   *
   * @param p the point of interest
   * @return the cell that would contain p
   */
  public SpatialQuadTree getContainingQuadTreeLeaf(Point2D p) {
    return getContainingQuadTreeLeaf(p.getX(), p.getY());
  }

  /**
   * @param x location of interest
   * @param y location of interest
   * @return the cell that would contain (x, y)
   */
  public SpatialQuadTree getContainingQuadTreeLeaf(double x, double y) {
    if (this.area.contains(x, y)) {
      if (this.children != null) {
        for (Map.Entry> entry : this.children.entrySet()) {
          if (entry.getValue().area.contains(x, y)) {
            return entry.getValue().getContainingQuadTreeLeaf(x, y);
          }
        }
      } else {
        // i am a leaf. return myself
        return this;
      }
    }
    return null;
  }

  @Override
  public N getClosestElement(Point2D p) {
    return getClosestElement(p.getY(), p.getY());
  }

  /**
   * get the node that is closest to the passed (x,y)
   *
   * @param x
   * @param y
   * @return the node closest to x,y
   */
  @Override
  public N getClosestElement(double x, double y) {
    if (!isActive()) {
      return fallback.getNode(layoutModel, x, y);
    }
    Spatial leaf = getContainingQuadTreeLeaf(x, y);
    Rectangle2D area = leaf.getLayoutArea();
    double radius = area.getWidth();
    N closest = null;
    while (closest == null) {

      double diameter = radius * 2;

      Ellipse2D searchArea = new Ellipse2D.Double(x - radius, y - radius, diameter, diameter);

      Collection nodes = getVisibleElements(searchArea);
      closest = getClosest(nodes, x, y, radius);

      // if I have already considered all of the nodes in the graph
      // (in the spatialquadtree) there is no reason to enlarge the
      // area and try again
      if (nodes.size() >= layoutModel.getGraph().nodes().size()) {
        break;
      }
      // double the search area size and try again
      radius *= 2;
    }
    return closest;
  }

  /**
   * reset the side of this structure
   *
   * @param bounds the new bounds for the data struture
   */
  @Override
  public void setBounds(Rectangle2D bounds) {
    gridCache = null;
    this.area = bounds;
  }

  /**
   * Update the structure for the passed node. If the node is still in the same cell, don't rebuild
   * the structure. If it moved to a new cell, rebuild the structure
   *
   * @param node
   */
  @Override
  public void update(N node, Point location) {
    if (isActive()) {
      gridCache = null;
      if (!this.getLayoutArea().contains(location.x, location.y)) {
        log.trace(location + " outside of spatial " + this.getLayoutArea());
        this.setBounds(this.getUnion(this.getLayoutArea(), location.x, location.y));
        this.recalculate(layoutModel.getGraph().nodes());
      }
      Spatial locationContainingLeaf = getContainingQuadTreeLeaf(location.x, location.y);
      log.trace("leaf {} contains {}", locationContainingLeaf, location);
      TreeNode nodeContainingLeaf = getContainingQuadTreeLeaf(node);
      log.trace("leaf {} contains node {}", nodeContainingLeaf, node);
      if (locationContainingLeaf == null) {
        log.trace("got null for leaf containing {}", location);
      }
      if (nodeContainingLeaf == null) {
        log.trace("got null for leaf containing {}", node);
      }
      if (locationContainingLeaf != null && !locationContainingLeaf.equals(nodeContainingLeaf)) {
        log.trace("time to recalculate");
        this.recalculate(layoutModel.getGraph().nodes());
      }
      this.insert(node);
    }
  }

  @Override
  public void layoutChanged(LayoutEvent evt) {
    Point location = evt.getLocation();
    N node = evt.getNode();
    this.update(node, evt.getLocation());
  }

  @Override
  public void layoutChanged(LayoutNetworkEvent evt) {
    this.update(evt.getNode(), evt.getLocation());
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    SpatialQuadTree that = (SpatialQuadTree) o;

    if (level != that.level) return false;
    if (!nodes.equals(that.nodes)) return false;
    if (!area.equals(that.area)) return false;
    return layoutModel.equals(that.layoutModel);
  }

  @Override
  public int hashCode() {
    int result = level;
    result = 31 * result + nodes.hashCode();
    result = 31 * result + area.hashCode();
    result = 31 * result + layoutModel.hashCode();
    return result;
  }

  @Override
  public String toString() {
    return "SpatialQuadTree{"
        + "level="
        + level
        + ", nodes="
        + nodes
        + ", area="
        + area
        + ", children="
        + children
        + '}';
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy