/*******************************************************************************
* Copyright (c) 2007, 2010 Association for Decentralized Information Management
* in Industry THTH ry.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VTT Technical Research Centre of Finland - initial API and implementation
*******************************************************************************/
package org.simantics.scenegraph.g2d.nodes.spatial;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.simantics.scenegraph.g2d.G2DParentNode;
import org.simantics.scenegraph.g2d.G2DRenderingHints;
import org.simantics.scenegraph.g2d.IG2DNode;
import org.simantics.scenegraph.g2d.events.Event;
import org.simantics.scenegraph.g2d.events.EventTypes;
import org.simantics.scenegraph.g2d.events.INodeEventHandlerProvider;
import org.simantics.scenegraph.g2d.events.NodeEventHandler;
import org.simantics.scenegraph.utils.GeometryUtils;
import org.simantics.scenegraph.utils.NodeUtil;
import com.infomatiq.jsi.Rectangle;
import com.infomatiq.jsi.rtree.RTree;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntProcedure;
/**
* A G2D scene graph node that spatially decomposes all of its immediate child
* nodes into an R-Tree structure to optimize the painting of its direct child
* based on whether they are visible in the current clip-bounds or not.
*
*
* The clipping boundary, i.e. the current viewport is retrieved from
* {@link Graphics2D#getClip()}.
*
*
*
* {@link #setDirty()} will mark the whole r-tree invalid meaning it will be
* reconstructed from scratch when it is needed again.
*
*
* TODO:
*
* - Add interface for marking children added/removed/changed to optimize
* updates
* - With incremental updating, make sure that the r-tree is rebuilt every
* once in a while. Updates will most likely cause r-tree performance to slowly
* deteriorate.
*
*
* @author Tuukka Lehtonen
*/
public class RTreeNode extends G2DParentNode implements INodeEventHandlerProvider {
private static final boolean DISABLE_RTREE = false;
private static final boolean DEBUG_SHRINK_CLIP_RECT = false;
private static final long serialVersionUID = 8988645670981494042L;
/**
* A container for everything related to the R-tree based spatial
* decomposition.
*/
private static class Tree {
public final RTree rtree;
/**
* Bounds of the whole R-tree.
*/
public final Rectangle bounds;
/**
* May be null.
*/
public final ArrayList boundlessNodes;
/**
* Map from R-tree rectangle ID's to scene graph nodes.
*/
public final TIntObjectHashMap toNodes;
/**
* Map from R-tree rectangle ID's to current scene graph node bounds.
* This prevents the need to unnecessarily recalculate the node bounds
* in the node selection loop.
*/
public final TIntObjectHashMap toBounds;
public Tree(RTree rtree, ArrayList boundlessNodes, TIntObjectHashMap toNodes, TIntObjectHashMap toBounds) {
this.rtree = rtree;
this.bounds = rtree.getBounds();
this.boundlessNodes = boundlessNodes;
this.toNodes = toNodes;
this.toBounds = toBounds;
}
}
private transient volatile Tree tree = null;
private transient ArrayList collected = new ArrayList();
private transient Set simplified = new HashSet();
@Override
public void render(Graphics2D g) {
if (DISABLE_RTREE) {
super.render(g);
return;
}
AffineTransform ot = null;
if (!transform.isIdentity()) {
ot = g.getTransform();
g.transform(transform);
}
g.setRenderingHint(G2DRenderingHints.KEY_TRANSFORM_UNDER_SPATIAL_ROOT, g.getTransform());
try {
// Get transformed clip bounds
Shape clipShape = g.getClip();
Rectangle2D bounds = null;
if (clipShape instanceof Rectangle2D)
bounds = (Rectangle2D) clipShape;
else if (clipShape != null)
bounds = clipShape.getBounds2D();
// Calculate values to optimize rendering based on view scale
final double viewScale = GeometryUtils.getScale(g.getTransform());
if (Math.abs(viewScale) <= Double.MIN_VALUE)
return;
final double unitsPerPixel = 1.0 / viewScale;
//final double simplificationUnitsPerPixelLimit = 4*unitsPerPixel;
//System.out.println("view scale: " + viewScale + " - " + unitsPerPixel + " - " + simplificationUnitsPerPixelLimit);
// A bit of added margin for the clipping to
// prevent the system from clipping symbols too
// early in case they have out-of-bounds decorations.
if (bounds != null)
GeometryUtils.expandRectangle(bounds, 5);
// !DEBUG!
if (DEBUG_SHRINK_CLIP_RECT) {
GeometryUtils.expandRectangle(bounds, -100.0*unitsPerPixel);
g.draw(bounds);
}
// Make sure that a spatial decomposition exists.
final Tree tree = getSpatialDecomposition();
if (bounds == null || tree.bounds == null || containedBy(tree.bounds, bounds)) {
// Direct render if no pruning can be done.
for (IG2DNode node : getSortedNodes()) {
if (node.validate()) {
node.render(g);
}
}
} else {
final Object render = g.getRenderingHint(RenderingHints.KEY_RENDERING);
collected.clear();
if (tree.boundlessNodes != null) {
for (int i = 0, n = tree.boundlessNodes.size(); i < n; ++i)
collected.add(tree.boundlessNodes.get(i));
}
tree.rtree.intersects(toRectangle(bounds), new TIntProcedure() {
@Override
public boolean execute(int value) {
//System.out.println("exec: " + value);
IG2DNode node = tree.toNodes.get(value);
//System.out.println(" node: " + node);
if (node == null || !node.validate())
return true;
if (render != RenderingHints.VALUE_RENDER_QUALITY) {
Rectangle2D r = tree.toBounds.get(value);
if (r == null)
return true;
double w = r.getWidth();
double h = r.getHeight();
//System.out.println("size: " + w + " x " + h);
if (w < unitsPerPixel && h < unitsPerPixel) {
//System.out.println("Skipping node: " + node);
return true;
}
// if (w < simplificationUnitsPerPixelLimit && h < simplificationUnitsPerPixelLimit) {
// //System.out.println("simplifying node: " + node);
// simplified.add(node);
// }
}
collected.add(node);
return true;
}
});
Collections.sort(collected, G2DParentNode.G2DNODE_Z_COMPARATOR);
//System.out.println("rendering " + collected.size() + "/" + getNodeCount() + " children, " + simplified.size() + " as simplified");
if (simplified.isEmpty()) {
for (IG2DNode node : collected)
if (node.validate())
node.render(g);
} else {
for (IG2DNode node : collected) {
if (node.validate()) {
if (simplified.contains(node)) {
g.draw(node.getBoundsInLocal());
} else {
node.render(g);
}
}
}
simplified.clear();
}
}
// // !DEBUG / PROFILING!
// g.setStroke(new BasicStroke(0.25f));
// drawTree(g, tree.rtree);
} finally {
g.setRenderingHint(G2DRenderingHints.KEY_TRANSFORM_UNDER_SPATIAL_ROOT, null);
if (ot != null)
g.setTransform(ot);
}
}
// private void drawTree(final Graphics2D g, RTree rtree) {
// final Rectangle2D r = new Rectangle2D.Double();
// tree.rtree.traverse(new IRectangleTraverseProcedure() {
// @Override
// public void call(double x0, double y0, double x1, double y1, int level, Object value) {
// r.setFrameFromDiagonal(x0, y0, x1, y1);
// g.setColor(getLevelColor(level));
// g.draw(r);
// }
//
// private Color getLevelColor(int level) {
// switch (level) {
// case 0: return Color.DARK_GRAY;
// case 1: return Color.RED;
// case 2: return Color.PINK;
// case 3: return Color.BLUE;
// case 4: return Color.ORANGE;
// default: return Color.BLACK;
// }
// }
// });
// }
@ClientSide
public void setDirty() {
tree = null;
}
private Tree getSpatialDecomposition() {
Tree t = tree;
if (t == null) {
t = decompose();
tree = t;
}
return t;
}
Properties props = new Properties();
private Tree decompose() {
RTree rtree = new RTree();
rtree.init(props);
IG2DNode[] nodes = getSortedNodes();
TIntObjectHashMap toNodes = new TIntObjectHashMap(nodes.length);
TIntObjectHashMap toBounds = new TIntObjectHashMap(nodes.length);
int rid = 0;
ArrayList boundlessNodes = null;
for (IG2DNode node : nodes) {
Rectangle2D bounds = node.getBounds();
if (bounds != null) {
Rectangle r = toRectangle(bounds);
int id = ++rid;
rtree.add(r, id);
toNodes.put(id, node);
toBounds.put(id, bounds);
} else {
if (boundlessNodes == null)
boundlessNodes = new ArrayList();
boundlessNodes.add(node);
}
}
return new Tree(rtree, boundlessNodes, toNodes, toBounds);
}
/**
* @param rect
* @return
*/
public static Rectangle toRectangle(Rectangle2D rect) {
return new Rectangle((float) rect.getMinX(), (float) rect.getMinY(), (float) rect.getMaxX(), (float) rect.getMaxY());
}
public List intersectingNodes(Rectangle2D rect, List result) {
final Tree tree = getSpatialDecomposition();
if (rect == null || tree.bounds == null || containedBy(tree.bounds, rect)) {
IG2DNode[] nodes = getSortedNodes();
for (IG2DNode node : nodes) {
if (node.validate()) {
result.add(node);
}
}
} else {
tree.rtree.intersects(toRectangle(rect), value -> {
//System.out.println("exec: " + value);
IG2DNode node = tree.toNodes.get(value);
//System.out.println(" node: " + node);
if (node == null || !node.validate())
return true;
result.add(node);
return true;
});
}
Collections.sort(result, G2DParentNode.G2DNODE_Z_COMPARATOR);
return result;
}
/**
* Determine whether this rectangle is contained by the passed rectangle
*
* @param contained the rectangle tested to see if it contained by container
* @param container The rectangle that might contain this rectangle
*
* @return true
if the container contains contained,
* false
if it does not
*/
public static boolean containedBy(Rectangle contained, Rectangle2D container) {
return container.getMaxX() >= contained.maxX && container.getMinX() <= contained.minX
&& container.getMaxY() >= contained.maxY && container.getMinY() <= contained.minY;
}
@Override
public void init() {
super.init();
//addEventHandler(this);
}
@Override
public void cleanup() {
//removeEventHandler(this);
super.cleanup();
}
@Override
public int getEventMask() {
return EventTypes.MouseMask;
}
@Override
public boolean handleEvent(Event e) {
// Propagate mouse events to children based on bounds.
//System.out.println("R-TREE HANDLE EVENT: " + e);
return false;
}
@Override
public NodeEventHandler getEventHandler() {
// TODO: add an event handler here and propagate mouse events spatially
return NodeUtil.getNodeEventHandler(this);
}
}