1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.g2d.diagram.handler.impl;
14 import java.awt.Shape;
15 import java.awt.geom.AffineTransform;
16 import java.awt.geom.NoninvertibleTransformException;
17 import java.awt.geom.Rectangle2D;
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.HashSet;
21 import java.util.List;
23 import java.util.Objects;
25 import java.util.function.Predicate;
26 import java.util.stream.Collectors;
28 import org.simantics.g2d.diagram.DiagramHints;
29 import org.simantics.g2d.diagram.IDiagram;
30 import org.simantics.g2d.diagram.handler.PickContext;
31 import org.simantics.g2d.diagram.handler.PickRequest;
32 import org.simantics.g2d.diagram.handler.PickRequest.PickPolicy;
33 import org.simantics.g2d.element.ElementClass;
34 import org.simantics.g2d.element.ElementHints;
35 import org.simantics.g2d.element.ElementUtils;
36 import org.simantics.g2d.element.IElement;
37 import org.simantics.g2d.element.handler.ElementLayers;
38 import org.simantics.g2d.element.handler.InternalSize;
39 import org.simantics.g2d.element.handler.Outline;
40 import org.simantics.g2d.element.handler.Pick;
41 import org.simantics.g2d.element.handler.Pick2;
42 import org.simantics.g2d.element.handler.Transform;
43 import org.simantics.g2d.layers.ILayers;
44 import org.simantics.g2d.scenegraph.SceneGraphConstants;
45 import org.simantics.g2d.utils.GeometryUtils;
46 import org.simantics.scenegraph.INode;
47 import org.simantics.scenegraph.g2d.IG2DNode;
48 import org.simantics.scenegraph.g2d.nodes.spatial.RTreeNode;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * @author Toni Kalajainen
55 public class PickContextImpl implements PickContext {
57 public static final PickContextImpl INSTANCE = new PickContextImpl();
59 private static final Logger LOGGER = LoggerFactory.getLogger(PickContextImpl.class);
61 private static final boolean PERF = false;
63 private static final ThreadLocal<Rectangle2D> perThreadElementBounds = ThreadLocal.withInitial(() -> new Rectangle2D.Double());
69 Collection<IElement> finalResult)
71 assert(diagram!=null);
72 assert(request!=null);
73 assert(finalResult!=null);
75 long startTime = PERF ? System.nanoTime() : 0L;
77 List<IElement> result = pickElements(diagram, request);
80 long endTime = System.nanoTime();
81 LOGGER.info("[picked " + result.size() + " elements @ " + request.pickArea + "] total pick time : " + ((endTime - startTime)*1e-6));
84 if (!result.isEmpty()) {
85 if (request.pickSorter != null) {
86 List<IElement> elems = new ArrayList<>(result);
87 request.pickSorter.sort(elems);
88 finalResult.addAll(elems);
90 finalResult.addAll(result);
95 private static class PickFilter implements Predicate<IElement> {
97 private final PickRequest request;
98 private final ILayers layers;
99 private final boolean checkLayers;
101 public PickFilter(PickRequest request, ILayers layers) {
102 this.request = request;
103 this.layers = layers;
104 this.checkLayers = layers != null && !layers.getIgnoreFocusSettings();
108 public boolean test(IElement e) {
109 // Ignore hidden elements.
110 if (ElementUtils.isHidden(e))
113 ElementClass ec = e.getElementClass();
116 ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
117 if (el != null && !el.isFocusable(e, layers))
121 if (request.pickFilter != null && !request.pickFilter.accept(e))
124 // Get InternalSize for retrieving bounds, ignore elements that have no bounds
125 InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class);
129 // Anything that is without a transformation is not pickable
130 AffineTransform canvasToElement = getTransform(e);
131 if (canvasToElement == null)
134 Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement);
135 if (elementBoundsOnCanvas == null)
138 // Pick with pick handler(s)
139 List<Pick> pickHandlers = ec.getItemsByClass(Pick.class);
140 if (!pickHandlers.isEmpty()) {
141 // Rough filtering with bounds
142 if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) {
143 // System.out.println("Element bounds discards " + e.getElementClass());
147 // NOTE: this doesn't support Pick2 interface anymore
148 for (Pick p : pickHandlers) {
149 if (p.pickTest(e, request.pickArea, request.pickPolicy)) {
156 // Pick with Outline handler(s)
157 List<Outline> outlineHandlers = ec.getItemsByClass(Outline.class);
158 if (!outlineHandlers.isEmpty())
159 return pickByOutline(e, request, elementBoundsOnCanvas, canvasToElement, outlineHandlers);
161 // Finally, pick by rectangle
162 return pickByBounds(e, request, elementBoundsOnCanvas);
167 private static Rectangle2D toBoundingRectangle(Shape shape) {
168 if (shape instanceof Rectangle2D)
169 return (Rectangle2D) shape;
171 return shape.getBounds2D();
174 private static List<IElement> pickElements(IDiagram diagram, PickRequest request) {
175 ILayers layers = diagram.getHint(DiagramHints.KEY_LAYERS);
177 // Get the scene graph nodes that intersect the pick-requests pick shape
178 INode spatialRoot = request.pickContext != null
179 ? request.pickContext.getSceneGraph().lookupNode(SceneGraphConstants.SPATIAL_ROOT_NODE_ID)
182 if (spatialRoot instanceof RTreeNode) {
183 // Optimized picking version that no longer supports Pick2 interface
184 // and therefore doesn't support connections modelled as
185 // branchpoint/edge subelements of connection elements.
187 RTreeNode rtree = (RTreeNode) spatialRoot;
188 Map<INode, IElement> nodeToElement = diagram.getHint(DiagramHints.NODE_TO_ELEMENT_MAP);
189 if (nodeToElement != null) {
190 // The most optimized version
191 return rtree.intersectingNodes(toBoundingRectangle(request.pickArea), new ArrayList<>())
194 .map(nodeToElement::get)
195 .filter(Objects::nonNull)
196 .filter(new PickFilter(request, layers))
197 .collect(Collectors.toList());
199 // Slower version for when DiagramHints.NODE_TO_ELEMENT_MAP is not used
200 Set<IG2DNode> nodes =
202 rtree.intersectingNodes(
203 toBoundingRectangle(request.pickArea),
207 return diagram.getSnapshot().stream()
209 // Choose only elements that are under the pick region bounds-wise
211 INode node = e.getHint(ElementHints.KEY_SG_NODE);
212 return nodes.contains(node);
214 // Perform comprehensive picking
215 .filter(new PickFilter(request, layers))
216 .collect(Collectors.toList());
221 // Fall-back logic that ends up processing everything.
222 // This still supports all the old logic and the Pick2
223 // interface in element classes.
224 List<IElement> result = new ArrayList<>();
225 boolean checkLayers = layers != null && !layers.getIgnoreFocusSettings();
227 // Do not do this in parallel mode to keep results in proper Z order
228 diagram.getSnapshot().stream().forEachOrdered(e -> {
229 // Ignore hidden elements.
230 if (ElementUtils.isHidden(e))
233 ElementClass ec = e.getElementClass();
236 ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
237 if (el != null && !el.isFocusable(e, layers))
241 if (request.pickFilter != null && !request.pickFilter.accept(e))
244 // Get InternalSize for retrieving bounds, ignore elements that have no bounds
245 InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class);
249 // Anything that is without a transformation is not pickable
250 AffineTransform canvasToElement = getTransform(e);
251 if (canvasToElement == null)
254 Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement);
255 if (elementBoundsOnCanvas == null)
258 // Pick with pick handler(s)
259 List<Pick> pickHandlers = ec.getItemsByClass(Pick.class);
260 if (!pickHandlers.isEmpty()) {
261 // Rough filtering with bounds
262 if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) {
263 // System.out.println("Element bounds discards " + e.getElementClass());
267 for (Pick p : pickHandlers) {
268 if (p instanceof Pick2) {
269 Pick2 p2 = (Pick2) p;
270 if (p2.pick(e, request.pickArea, request.pickPolicy, result) > 0)
273 if (p.pickTest(e, request.pickArea, request.pickPolicy)) {
282 // Pick with shape handler(s)
283 List<Outline> outlineHandlers = ec.getItemsByClass(Outline.class);
284 if (!outlineHandlers.isEmpty()) {
285 if (pickByOutline(e, request, elementBoundsOnCanvas, canvasToElement, outlineHandlers)) {
291 // Finally, pick by bounding rectangle
292 if (pickByBounds(e, request, elementBoundsOnCanvas))
301 private static AffineTransform getTransform(IElement e) {
302 // Anything that is without a transformation is not pickable
303 Transform t = e.getElementClass().getSingleItem(Transform.class);
304 AffineTransform canvasToElement = t.getTransform(e);
305 if (canvasToElement == null)
308 double det = canvasToElement.getDeterminant();
310 // Singular transform, only take the translation from it to move on.
311 canvasToElement = AffineTransform.getTranslateInstance(canvasToElement.getTranslateX(), canvasToElement.getTranslateY());
314 return canvasToElement;
317 private static Rectangle2D getElementBounds(IElement e, InternalSize b, AffineTransform transform) {
318 Rectangle2D elementBounds = perThreadElementBounds.get();
319 elementBounds.setFrame(Double.NaN, Double.NaN, Double.NaN, Double.NaN);
320 b.getBounds(e, elementBounds);
321 if (Double.isNaN(elementBounds.getWidth()) || Double.isNaN(elementBounds.getHeight()))
324 // Get optionally transformed axis-aligned bounds of the element,
325 // expanded by 1mm in each direction.
326 Rectangle2D transformedBounds = transform != null
327 ? toBoundingRectangle(GeometryUtils.transformShape(elementBounds, transform))
329 return org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(transformedBounds, 1e-3);
332 private static boolean pickByOutline(IElement e, PickRequest request, Rectangle2D elementBoundsOnCanvas, AffineTransform canvasToElement, List<Outline> outlineHandlers) {
333 // Rough filtering with bounds
334 if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas))
337 // Convert pick shape to element coordinates
338 AffineTransform elementToCanvas;
340 elementToCanvas = canvasToElement.createInverse();
341 } catch (NoninvertibleTransformException ex) {
342 throw new RuntimeException(ex);
344 Shape pickShapeInElementCoords = GeometryUtils.transformShape(request.pickArea, elementToCanvas);
346 // Intersection with one shape is enough
347 if (request.pickPolicy == PickPolicy.PICK_INTERSECTING_OBJECTS) {
348 for (Outline es : outlineHandlers) {
349 Shape elementShape = es.getElementShape(e);
350 if (elementShape == null)
352 if (GeometryUtils.intersects(pickShapeInElementCoords, elementShape)) {
359 // Contains of all shapes is required
360 if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) {
361 for (Outline es : outlineHandlers) {
362 Shape elementShape = es.getElementShape(e);
363 if (!GeometryUtils.contains(pickShapeInElementCoords, elementShape))
372 private static boolean pickByBounds(IElement e, PickRequest request, Rectangle2D elementBoundsOnCanvas) {
373 if (request.pickPolicy == PickPolicy.PICK_INTERSECTING_OBJECTS) {
374 if (GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas))
376 } else if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) {
377 if (GeometryUtils.contains(request.pickArea, elementBoundsOnCanvas))