1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.scenegraph.g2d.nodes.spatial;
\r
14 import gnu.trove.TIntObjectHashMap;
\r
15 import gnu.trove.TIntProcedure;
\r
17 import java.awt.Graphics2D;
\r
18 import java.awt.RenderingHints;
\r
19 import java.awt.Shape;
\r
20 import java.awt.geom.AffineTransform;
\r
21 import java.awt.geom.Rectangle2D;
\r
22 import java.util.ArrayList;
\r
23 import java.util.Collections;
\r
24 import java.util.HashSet;
\r
25 import java.util.Properties;
\r
26 import java.util.Set;
\r
28 import org.simantics.scenegraph.g2d.G2DParentNode;
\r
29 import org.simantics.scenegraph.g2d.IG2DNode;
\r
30 import org.simantics.scenegraph.g2d.events.Event;
\r
31 import org.simantics.scenegraph.g2d.events.EventTypes;
\r
32 import org.simantics.scenegraph.g2d.events.INodeEventHandlerProvider;
\r
33 import org.simantics.scenegraph.g2d.events.NodeEventHandler;
\r
34 import org.simantics.scenegraph.utils.GeometryUtils;
\r
35 import org.simantics.scenegraph.utils.NodeUtil;
\r
37 import com.infomatiq.jsi.Rectangle;
\r
38 import com.infomatiq.jsi.rtree.RTree;
\r
41 * A G2D scene graph node that spatially decomposes all of its immediate child
\r
42 * nodes into an R-Tree structure to optimize the painting of its direct child
\r
43 * based on whether they are visible in the current clip-bounds or not.
\r
46 * The clipping boundary, i.e. the current viewport is retrieved from
\r
47 * {@link Graphics2D#getClip()}.
\r
51 * {@link #setDirty()} will mark the whole r-tree invalid meaning it will be
\r
52 * reconstructed from scratch when it is needed again.
\r
57 * <li>Add interface for marking children added/removed/changed to optimize
\r
59 * <li>With incremental updating, make sure that the r-tree is rebuilt every
\r
60 * once in a while. Updates will most likely cause r-tree performance to slowly
\r
64 * @author Tuukka Lehtonen
\r
66 public class RTreeNode extends G2DParentNode implements INodeEventHandlerProvider {
\r
68 private static final boolean DISABLE_RTREE = false;
\r
70 private static final boolean DEBUG_SHRINK_CLIP_RECT = false;
\r
72 private static final long serialVersionUID = 8988645670981494042L;
\r
75 * A container for everything related to the R-tree based spatial
\r
78 private static class Tree {
\r
79 public final RTree rtree;
\r
82 * Bounds of the whole R-tree.
\r
84 public final Rectangle bounds;
\r
89 public final ArrayList<IG2DNode> boundlessNodes;
\r
92 * Map from R-tree rectangle ID's to scene graph nodes.
\r
94 public final TIntObjectHashMap<IG2DNode> toNodes;
\r
97 * Map from R-tree rectangle ID's to current scene graph node bounds.
\r
98 * This prevents the need to unnecessarily recalculate the node bounds
\r
99 * in the node selection loop.
\r
101 public final TIntObjectHashMap<Rectangle2D> toBounds;
\r
103 public Tree(RTree rtree, ArrayList<IG2DNode> boundlessNodes, TIntObjectHashMap<IG2DNode> toNodes, TIntObjectHashMap<Rectangle2D> toBounds) {
\r
104 this.rtree = rtree;
\r
105 this.bounds = rtree.getBounds();
\r
106 this.boundlessNodes = boundlessNodes;
\r
107 this.toNodes = toNodes;
\r
108 this.toBounds = toBounds;
\r
112 private transient volatile Tree tree = null;
\r
113 private transient ArrayList<IG2DNode> collected = new ArrayList<IG2DNode>();
\r
114 private transient Set<IG2DNode> simplified = new HashSet<IG2DNode>();
\r
117 public void render(Graphics2D g) {
\r
118 if (DISABLE_RTREE) {
\r
123 AffineTransform ot = null;
\r
124 if (!transform.isIdentity()) {
\r
125 ot = g.getTransform();
\r
126 g.transform(transform);
\r
130 // Get transformed clip bounds
\r
131 Shape clipShape = g.getClip();
\r
132 Rectangle2D bounds = null;
\r
133 if (clipShape instanceof Rectangle2D)
\r
134 bounds = (Rectangle2D) clipShape;
\r
135 else if (clipShape != null)
\r
136 bounds = clipShape.getBounds2D();
\r
138 // Calculate values to optimize rendering based on view scale
\r
139 final double viewScale = GeometryUtils.getScale(g.getTransform());
\r
140 if (Math.abs(viewScale) <= Double.MIN_VALUE)
\r
142 final double unitsPerPixel = 1.0 / viewScale;
\r
143 //final double simplificationUnitsPerPixelLimit = 4*unitsPerPixel;
\r
144 //System.out.println("view scale: " + viewScale + " - " + unitsPerPixel + " - " + simplificationUnitsPerPixelLimit);
\r
146 // A bit of added margin for the clipping to
\r
147 // prevent the system from clipping symbols too
\r
148 // early in case they have out-of-bounds decorations.
\r
149 if (bounds != null)
\r
150 GeometryUtils.expandRectangle(bounds, 5);
\r
153 if (DEBUG_SHRINK_CLIP_RECT) {
\r
154 GeometryUtils.expandRectangle(bounds, -100.0*unitsPerPixel);
\r
158 // Make sure that a spatial decomposition exists.
\r
159 final Tree tree = getSpatialDecomposition();
\r
161 if (bounds == null || tree.bounds == null || containedBy(tree.bounds, bounds)) {
\r
162 // Direct render if no pruning can be done.
\r
163 for (IG2DNode node : getSortedNodes()) {
\r
164 if (node.validate()) {
\r
169 final Object render = g.getRenderingHint(RenderingHints.KEY_RENDERING);
\r
173 if (tree.boundlessNodes != null) {
\r
174 for (int i = 0, n = tree.boundlessNodes.size(); i < n; ++i)
\r
175 collected.add(tree.boundlessNodes.get(i));
\r
178 tree.rtree.intersects(toRectangle(bounds), new TIntProcedure() {
\r
181 public boolean execute(int value) {
\r
182 //System.out.println("exec: " + value);
\r
183 IG2DNode node = tree.toNodes.get(value);
\r
184 //System.out.println(" node: " + node);
\r
185 if (node == null || !node.validate())
\r
188 if (render != RenderingHints.VALUE_RENDER_QUALITY) {
\r
189 Rectangle2D r = tree.toBounds.get(value);
\r
193 double w = r.getWidth();
\r
194 double h = r.getHeight();
\r
196 //System.out.println("size: " + w + " x " + h);
\r
197 if (w < unitsPerPixel && h < unitsPerPixel) {
\r
198 //System.out.println("Skipping node: " + node);
\r
202 // if (w < simplificationUnitsPerPixelLimit && h < simplificationUnitsPerPixelLimit) {
\r
203 // //System.out.println("simplifying node: " + node);
\r
204 // simplified.add(node);
\r
208 collected.add(node);
\r
212 Collections.sort(collected, G2DParentNode.G2DNODE_Z_COMPARATOR);
\r
213 //System.out.println("rendering " + collected.size() + "/" + getNodeCount() + " children, " + simplified.size() + " as simplified");
\r
214 if (simplified.isEmpty()) {
\r
215 for (IG2DNode node : collected)
\r
216 if (node.validate())
\r
219 for (IG2DNode node : collected) {
\r
220 if (node.validate()) {
\r
221 if (simplified.contains(node)) {
\r
222 g.draw(node.getBoundsInLocal());
\r
228 simplified.clear();
\r
232 // // !DEBUG / PROFILING!
\r
233 // g.setStroke(new BasicStroke(0.25f));
\r
234 // drawTree(g, tree.rtree);
\r
238 g.setTransform(ot);
\r
242 // private void drawTree(final Graphics2D g, RTree rtree) {
\r
243 // final Rectangle2D r = new Rectangle2D.Double();
\r
244 // tree.rtree.traverse(new IRectangleTraverseProcedure() {
\r
246 // public void call(double x0, double y0, double x1, double y1, int level, Object value) {
\r
247 // r.setFrameFromDiagonal(x0, y0, x1, y1);
\r
248 // g.setColor(getLevelColor(level));
\r
252 // private Color getLevelColor(int level) {
\r
253 // switch (level) {
\r
254 // case 0: return Color.DARK_GRAY;
\r
255 // case 1: return Color.RED;
\r
256 // case 2: return Color.PINK;
\r
257 // case 3: return Color.BLUE;
\r
258 // case 4: return Color.ORANGE;
\r
259 // default: return Color.BLACK;
\r
266 public void setDirty() {
\r
270 private Tree getSpatialDecomposition() {
\r
279 Properties props = new Properties();
\r
281 private Tree decompose() {
\r
282 RTree rtree = new RTree();
\r
284 IG2DNode[] nodes = getSortedNodes();
\r
285 TIntObjectHashMap<IG2DNode> toNodes = new TIntObjectHashMap<IG2DNode>(nodes.length);
\r
286 TIntObjectHashMap<Rectangle2D> toBounds = new TIntObjectHashMap<Rectangle2D>(nodes.length);
\r
288 ArrayList<IG2DNode> boundlessNodes = null;
\r
289 for (IG2DNode node : nodes) {
\r
290 Rectangle2D bounds = node.getBounds();
\r
291 if (bounds != null) {
\r
292 Rectangle r = toRectangle(bounds);
\r
295 toNodes.put(id, node);
\r
296 toBounds.put(id, bounds);
\r
298 if (boundlessNodes == null)
\r
299 boundlessNodes = new ArrayList<IG2DNode>();
\r
300 boundlessNodes.add(node);
\r
303 return new Tree(rtree, boundlessNodes, toNodes, toBounds);
\r
310 public static Rectangle toRectangle(Rectangle2D rect) {
\r
311 return new Rectangle((float) rect.getMinX(), (float) rect.getMinY(), (float) rect.getMaxX(), (float) rect.getMaxY());
\r
315 * Determine whether this rectangle is contained by the passed rectangle
\r
317 * @param contained the rectangle tested to see if it contained by container
\r
318 * @param container The rectangle that might contain this rectangle
\r
320 * @return <code>true</code> if the container contains contained,
\r
321 * <code>false</code> if it does not
\r
323 public static boolean containedBy(Rectangle contained, Rectangle2D container) {
\r
324 return container.getMaxX() >= contained.maxX && container.getMinX() <= contained.minX
\r
325 && container.getMaxY() >= contained.maxY && container.getMinY() <= contained.minY;
\r
329 public void init() {
\r
331 //addEventHandler(this);
\r
335 public void cleanup() {
\r
336 //removeEventHandler(this);
\r
341 public int getEventMask() {
\r
342 return EventTypes.MouseMask;
\r
346 public boolean handleEvent(Event e) {
\r
347 // Propagate mouse events to children based on bounds.
\r
348 //System.out.println("R-TREE HANDLE EVENT: " + e);
\r
353 public NodeEventHandler getEventHandler() {
\r
354 // TODO: add an event handler here and propagate mouse events spatially
\r
355 return NodeUtil.getNodeEventHandler(this);
\r