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

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

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

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import edu.uci.ics.jung.layout.model.LayoutModel;
import edu.uci.ics.jung.layout.model.Point;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A Spatial Data Structure to optimize rendering performance. The SpatialGrid is used to determine
 * which graph nodes are actually visible for a given rendering situation. Only the visible nodes
 * are passed to the rendering pipeline. When used with Edges, only Edges with at least one visible
 * endpoint are passed to the rendering pipeline.
 *
 * 

See SimpleGraphSpatialTest (jung-samples) for a rendering that exposes the internals of the * SpatialGrid. * * @author Tom Nelson */ public class SpatialGrid extends AbstractSpatial implements Spatial, TreeNode { private static final Logger log = LoggerFactory.getLogger(SpatialGrid.class); /** the number of grid cells across the width */ private int horizontalCount; /** the number of grid cells across the height */ private int verticalCount; /** the overall size of the area to be divided into a grid */ private Dimension size; /** A mapping of grid cell identified to a collection of contained nodes */ private Multimap map = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); /** the width of a grid cell */ private double boxWidth; /** the height of a grid cell */ private double boxHeight; /** the overall area of the layout (x,y,width,height) */ private Rectangle2D layoutArea; /** a cache of grid cell rectangles for performance */ private List gridCache; /** * Create an instance * * @param layoutModel */ public SpatialGrid(LayoutModel layoutModel) { super(layoutModel); this.horizontalCount = 10; this.verticalCount = 10; this.setBounds(new Rectangle2D.Double(0, 0, layoutModel.getWidth(), layoutModel.getHeight())); } /** * Create an instance * * @param bounds the area of the grid * @param horizontalCount how many tiles in a row * @param verticalCount how many tiles in a column */ public SpatialGrid( LayoutModel layoutModel, Rectangle2D bounds, int horizontalCount, int verticalCount) { super(layoutModel); this.horizontalCount = horizontalCount; this.verticalCount = verticalCount; this.setBounds(bounds); } /** * Set the layoutSize of the spatial grid and recompute the box widths and heights. null out the * obsolete grid cache * * @param bounds recalculate the size of the spatial area */ public void setBounds(Rectangle2D bounds) { this.size = bounds.getBounds().getSize(); this.layoutArea = bounds; this.boxWidth = size.getWidth() / horizontalCount; this.boxHeight = size.getHeight() / verticalCount; this.gridCache = null; } @Override public Set getContainingLeafs(Point2D p) { int boxNumber = this.getBoxNumberFromLocation(p.getX(), p.getY()); Rectangle2D r = (Rectangle2D) this.gridCache.get(boxNumber); SpatialGrid grid = new SpatialGrid(layoutModel, r, 1, 1); return Collections.singleton(grid); } @Override public Set getContainingLeafs(double x, double y) { return getContainingLeafs(new Point2D.Double(x, y)); } @Override public TreeNode getContainingLeaf(Object element) { for (Map.Entry> entry : map.asMap().entrySet()) { if (entry.getValue().contains(element)) { int index = entry.getKey(); Rectangle2D r = (Rectangle2D) this.gridCache.get(index); return new SpatialGrid<>(layoutModel, r, 1, 1); } } return null; } public static List getGrid(List list, SpatialGrid grid) { list.addAll(grid.getGrid()); return list; } /** * Lazily compute the gridCache if needed. The gridCache is a list of rectangles overlaying the * layout area. They are numbered from 0 to horizontalCount*verticalCount-1 * * @return the boxes in the grid */ @Override public List getGrid() { if (gridCache == null) { gridCache = Lists.newArrayList(); for (int j = 0; j < verticalCount; j++) { for (int i = 0; i < horizontalCount; i++) { gridCache.add( new Rectangle2D.Double( layoutArea.getX() + i * boxWidth, layoutArea.getY() + j * boxHeight, boxWidth, boxHeight)); } } } return gridCache; } /** * A Multimap of box number to Lists of nodes in that box * * @return the map of box numbers to contained nodes */ public Multimap getMap() { return map; } /** * given the box x,y coordinates (not the coordinate system) return the box number (0,0) has box 0 * (horizontalCount,horizontalCount) has box horizontalCount*verticalCount - 1 * * @param boxX the x value in the box grid * @param boxY the y value in the box grid * @return the box number for boxX,boxY */ protected int getBoxNumber(int boxX, int boxY) { if (log.isTraceEnabled()) { if (log.isTraceEnabled()) { log.trace( "{},{} clamped to {},{}", boxX, boxY, Math.max(0, Math.min(boxX, this.horizontalCount - 1)), Math.max(0, Math.min(boxY, this.verticalCount - 1))); } } boxX = Math.max(0, Math.min(boxX, this.horizontalCount - 1)); boxY = Math.max(0, Math.min(boxY, this.verticalCount - 1)); if (log.isTraceEnabled()) { log.trace("getBoxNumber({},{}):{}", boxX, boxY, (boxY * this.horizontalCount + boxX)); } return boxY * this.horizontalCount + boxX; } /** * @param boxXY the (x,y) in the grid coordinate system * @return the box number for that (x,y) */ protected int getBoxNumber(int[] boxXY) { return getBoxNumber(boxXY[0], boxXY[1]); } /** * give a Point in the coordinate system, return the box number that contains it * * @param p a location in the coordinate system * @return the box number that contains the passed location */ protected int getBoxNumberFromLocation(Point p) { int count = 0; for (Shape shape : getGrid()) { Rectangle2D r = shape.getBounds2D(); if (r.contains(p.x, p.y) || r.intersects(p.x, p.y, 1, 1)) { return count; } else { count++; } } log.trace("no box for {}", p); return -1; } /** * give a Point in the coordinate system, return the box number that contains it * * @param x, y a location in the coordinate system * @return the box number that contains the passed location */ protected int getBoxNumberFromLocation(double x, double y) { int count = 0; for (Shape shape : getGrid()) { Rectangle2D r = shape.getBounds2D(); if (r.contains(x, y) || r.intersects(x, y, 1, 1)) { return count; } else { count++; } } log.trace("no box for {},{}", x, y); return -1; } /** * given (x,y) in the coordinate system, get the boxX,boxY for the box that it is in * * @param x a location in the coordinate system * @param y a location in the coordinate system * @return a 2 dimensional array of int containing the box x and y coordinates */ protected int[] getBoxIndex(double x, double y) { // clamp the x and y to be within the bounds of the layout grid int[] boxIndex = new int[2]; int hcount = 0; int vcount = 0; for (Shape r : getGrid()) { if (r.contains(new Point2D.Double(x, y))) { boxIndex = new int[] {hcount, vcount}; break; } hcount++; if (hcount >= this.horizontalCount) { hcount = 0; vcount++; } } if (log.isTraceEnabled()) { log.trace("boxIndex for ({},{}) is {}", x, y, Arrays.toString(boxIndex)); } return boxIndex; } @Override public void recalculate() { if (isActive()) { recalculate(layoutModel.getGraph().nodes()); } } @Override public void clear() { this.map.clear(); } /** * Recalculate the contents of the Map of box number to contained Nodes * * @param nodes the collection of nodes to update in the structure */ public void recalculate(Collection nodes) { clear(); while (true) { try { for (N node : nodes) { this.map.put(this.getBoxNumberFromLocation(layoutModel.apply(node)), node); } break; } catch (ConcurrentModificationException ex) { // ignore } } } /** * update the location of a node in the map of box number to node lists * * @param node the node to update in the structure */ @Override public void update(N node, Point location) { if (isActive()) { 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)); recalculate(layoutModel.getGraph().nodes()); } int rightBox = this.getBoxNumberFromLocation(layoutModel.apply(node)); // node should end up in box 'rightBox' // check to see if it is already there if (map.get(rightBox).contains(node)) { // nothing to do here, just return return; } // remove node from the first (and only) wrong box it is found in Integer wrongBox = null; synchronized (map) { for (Integer box : map.keySet()) { if (map.get(box).contains(node)) { // remove it and stop, because node can be in only one box wrongBox = box; break; } } } if (wrongBox != null) { map.remove(wrongBox, node); } map.put(rightBox, node); } } @Override public N getClosestElement(Point2D p) { return getClosestElement(p.getX(), p.getY()); } @Override public N getClosestElement(double x, double y) { if (!isActive()) { return fallback.getNode(layoutModel, x, y); } Collection leafs = getContainingLeafs(x, y); if (leafs.size() != 0) { TreeNode leaf = leafs.iterator().next(); Rectangle2D area = leaf.getBounds(); 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; } return null; } /** * given a rectangular area and an offset, return the tile numbers that are contained in it * * @param visibleArea the (possibly) non-rectangular area of interest * @return the tile numbers that intersect with the visibleArea */ protected Collection getVisibleTiles(Shape visibleArea) { Set visibleTiles = Sets.newHashSet(); List grid = getGrid(); for (int i = 0; i < this.horizontalCount * this.verticalCount; i++) { if (visibleArea.intersects(grid.get(i).getBounds2D())) { visibleTiles.add(i); } } if (log.isDebugEnabled()) { log.debug("visible boxes:{}", visibleTiles); } return visibleTiles; } /** * Given an area, return a collection of the nodes that are contained in it (the nodes that are * contained in the boxes that intersect with the area) * * @param visibleArea a shape projected on the grid * @return the nodes that should be visible */ @Override public Set getVisibleElements(Shape visibleArea) { if (!isActive()) { log.trace("not active so getting from the graph"); return layoutModel.getGraph().nodes(); } pickShapes.add(visibleArea); Area area = new Area(visibleArea); area.intersect(new Area(this.layoutArea)); if (log.isTraceEnabled()) { log.trace("map is {}", map); } Set visibleNodes = Sets.newHashSet(); Collection tiles = getVisibleTiles(area); for (Integer index : tiles) { Collection toAdd = this.map.get(index); if (toAdd.size() > 0) { visibleNodes.addAll(toAdd); if (log.isTraceEnabled()) { log.trace("added all of: {} from index {} to visibleNodes", toAdd, index); } } } if (log.isDebugEnabled()) { log.debug("visibleNodes:{}", visibleNodes); } return visibleNodes; } /** @return the layout area rectangle for this grid */ @Override public Rectangle2D getLayoutArea() { return layoutArea; } @Override public Rectangle2D getBounds() { return layoutArea; } @Override public Collection getChildren() { return null; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy