1 /*******************************************************************************
2 * Copyright (c) 2007, 2020 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 * Semantum Oy - gitlab #60, #454 - parallel/spatial optimization
12 *******************************************************************************/
13 package org.simantics.g2d.diagram.handler.impl;
15 import java.awt.Shape;
16 import java.awt.geom.AffineTransform;
17 import java.awt.geom.NoninvertibleTransformException;
18 import java.awt.geom.Rectangle2D;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashSet;
22 import java.util.List;
24 import java.util.Objects;
26 import java.util.function.Predicate;
27 import java.util.stream.Collectors;
29 import org.simantics.g2d.diagram.DiagramHints;
30 import org.simantics.g2d.diagram.IDiagram;
31 import org.simantics.g2d.diagram.handler.PickContext;
32 import org.simantics.g2d.diagram.handler.PickRequest;
33 import org.simantics.g2d.diagram.handler.PickRequest.PickPolicy;
34 import org.simantics.g2d.element.ElementClass;
35 import org.simantics.g2d.element.ElementHints;
36 import org.simantics.g2d.element.ElementUtils;
37 import org.simantics.g2d.element.IElement;
38 import org.simantics.g2d.element.handler.ElementLayers;
39 import org.simantics.g2d.element.handler.InternalSize;
40 import org.simantics.g2d.element.handler.Outline;
41 import org.simantics.g2d.element.handler.Pick;
42 import org.simantics.g2d.element.handler.Pick2;
43 import org.simantics.g2d.element.handler.Transform;
44 import org.simantics.g2d.layers.ILayers;
45 import org.simantics.g2d.scenegraph.SceneGraphConstants;
46 import org.simantics.g2d.utils.GeometryUtils;
47 import org.simantics.scenegraph.INode;
48 import org.simantics.scenegraph.g2d.IG2DNode;
49 import org.simantics.scenegraph.g2d.nodes.spatial.RTreeNode;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * @author Toni Kalajainen
55 * @author Jani Simomaa
56 * @author Tuukka Lehtonen
58 public class PickContextImpl implements PickContext {
60 public static final PickContextImpl INSTANCE = new PickContextImpl();
62 private static final Logger LOGGER = LoggerFactory.getLogger(PickContextImpl.class);
64 private static final boolean PERF = false;
66 private static final ThreadLocal<Rectangle2D> perThreadElementBounds = ThreadLocal.withInitial(() -> new Rectangle2D.Double());
68 private boolean useRTree;
70 public PickContextImpl() {
74 public PickContextImpl(boolean useRTree) {
75 this.useRTree = useRTree;
82 Collection<IElement> finalResult)
84 assert(diagram!=null);
85 assert(request!=null);
86 assert(finalResult!=null);
88 long startTime = PERF ? System.nanoTime() : 0L;
90 List<IElement> result = pickElements(diagram, request);
93 long endTime = System.nanoTime();
94 LOGGER.info("[picked {} elements @ {}] total pick time {} ms", result.size(), request.pickArea, ((endTime - startTime)*1e-6));
97 if (!result.isEmpty()) {
98 if (request.pickSorter != null) {
99 List<IElement> elems = new ArrayList<>(result);
100 request.pickSorter.sort(request, elems);
101 finalResult.addAll(elems);
103 finalResult.addAll(result);
108 private static class PickFilter implements Predicate<IElement> {
110 private final PickRequest request;
111 private final ILayers layers;
112 private final boolean checkLayers;
114 public PickFilter(PickRequest request, ILayers layers) {
115 this.request = request;
116 this.layers = layers;
117 this.checkLayers = layers != null && !layers.getIgnoreFocusSettings();
121 public boolean test(IElement e) {
122 // Ignore hidden elements.
123 if (ElementUtils.isHidden(e))
126 ElementClass ec = e.getElementClass();
129 ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
130 if (el != null && !el.isFocusable(e, layers))
134 if (request.pickFilter != null && !request.pickFilter.accept(e))
137 // Get InternalSize for retrieving bounds, ignore elements that have no bounds
138 InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class);
142 // Anything that is without a transformation is not pickable
143 AffineTransform canvasToElement = getTransform(e);
144 if (canvasToElement == null)
147 Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement);
148 if (elementBoundsOnCanvas == null)
151 // Pick with pick handler(s)
152 List<Pick> pickHandlers = ec.getItemsByClass(Pick.class);
153 if (!pickHandlers.isEmpty()) {
154 // Rough filtering with bounds
155 if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) {
156 // System.out.println("Element bounds discards " + e.getElementClass());
160 // NOTE: this doesn't support Pick2 interface anymore
161 for (Pick p : pickHandlers) {
162 if (p.pickTest(e, request.pickArea, request.pickPolicy)) {
169 // Pick with Outline handler(s)
170 List<Outline> outlineHandlers = ec.getItemsByClass(Outline.class);
171 if (!outlineHandlers.isEmpty())
172 return pickByOutline(e, request, elementBoundsOnCanvas, canvasToElement, outlineHandlers);
174 // Finally, pick by rectangle
175 return pickByBounds(e, request, elementBoundsOnCanvas);
180 private static Rectangle2D toBoundingRectangle(Shape shape) {
181 if (shape instanceof Rectangle2D)
182 return (Rectangle2D) shape;
184 return shape.getBounds2D();
187 private List<IElement> pickElements(IDiagram diagram, PickRequest request) {
188 ILayers layers = diagram.getHint(DiagramHints.KEY_LAYERS);
190 // Get the scene graph nodes that intersect the pick-requests pick shape
191 INode spatialRoot = useRTree && request.pickContext != null
192 ? request.pickContext.getSceneGraph().lookupNode(SceneGraphConstants.SPATIAL_ROOT_NODE_ID)
195 if (spatialRoot instanceof RTreeNode) {
196 // Optimized picking version that no longer supports Pick2 interface
197 // and therefore doesn't support connections modelled as
198 // branchpoint/edge subelements of connection elements.
200 RTreeNode rtree = (RTreeNode) spatialRoot;
201 Map<INode, IElement> nodeToElement = diagram.getHint(DiagramHints.NODE_TO_ELEMENT_MAP);
202 if (nodeToElement != null) {
203 // The most optimized version
204 return rtree.intersectingNodes(toBoundingRectangle(request.pickArea), new ArrayList<>())
207 .map(nodeToElement::get)
208 .filter(Objects::nonNull)
209 .filter(new PickFilter(request, layers))
210 .collect(Collectors.toList());
212 // Slower version for when DiagramHints.NODE_TO_ELEMENT_MAP is not used
213 Set<IG2DNode> nodes =
215 rtree.intersectingNodes(
216 toBoundingRectangle(request.pickArea),
220 return diagram.getSnapshot().stream()
222 // Choose only elements that are under the pick region bounds-wise
224 INode node = e.getHint(ElementHints.KEY_SG_NODE);
225 return nodes.contains(node);
227 // Perform comprehensive picking
228 .filter(new PickFilter(request, layers))
229 .collect(Collectors.toList());
234 // Fall-back logic that ends up processing everything.
235 // This still supports all the old logic and the Pick2
236 // interface in element classes.
237 List<IElement> result = new ArrayList<>();
238 boolean checkLayers = layers != null && !layers.getIgnoreFocusSettings();
240 // Do not do this in parallel mode to keep results in proper Z order
241 diagram.getSnapshot().stream().forEachOrdered(e -> {
242 // Ignore hidden elements.
243 if (ElementUtils.isHidden(e))
246 ElementClass ec = e.getElementClass();
249 ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
250 if (el != null && !el.isFocusable(e, layers))
254 if (request.pickFilter != null && !request.pickFilter.accept(e))
257 // Get InternalSize for retrieving bounds, ignore elements that have no bounds
258 InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class);
262 // Anything that is without a transformation is not pickable
263 AffineTransform canvasToElement = getTransform(e);
264 if (canvasToElement == null)
267 Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement);
268 if (elementBoundsOnCanvas == null)
271 // Pick with pick handler(s)
272 List<Pick> pickHandlers = ec.getItemsByClass(Pick.class);
273 if (!pickHandlers.isEmpty()) {
274 // Rough filtering with bounds
275 if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) {
276 // System.out.println("Element bounds discards " + e.getElementClass());
280 for (Pick p : pickHandlers) {
281 if (p instanceof Pick2) {
282 Pick2 p2 = (Pick2) p;
283 if (p2.pick(e, request.pickArea, request.pickPolicy, result) > 0)
286 if (p.pickTest(e, request.pickArea, request.pickPolicy)) {
295 // Pick with shape handler(s)
296 List<Outline> outlineHandlers = ec.getItemsByClass(Outline.class);
297 if (!outlineHandlers.isEmpty()) {
298 if (pickByOutline(e, request, elementBoundsOnCanvas, canvasToElement, outlineHandlers)) {
304 // Finally, pick by bounding rectangle
305 if (pickByBounds(e, request, elementBoundsOnCanvas))
314 private static AffineTransform getTransform(IElement e) {
315 // Anything that is without a transformation is not pickable
316 Transform t = e.getElementClass().getSingleItem(Transform.class);
317 AffineTransform canvasToElement = t.getTransform(e);
318 if (canvasToElement == null)
321 double det = canvasToElement.getDeterminant();
323 // Singular transform, only take the translation from it to move on.
324 canvasToElement = AffineTransform.getTranslateInstance(canvasToElement.getTranslateX(), canvasToElement.getTranslateY());
327 return canvasToElement;
330 private static Rectangle2D getElementBounds(IElement e, InternalSize b, AffineTransform transform) {
331 Rectangle2D elementBounds = perThreadElementBounds.get();
332 elementBounds.setFrame(Double.NaN, Double.NaN, Double.NaN, Double.NaN);
333 b.getBounds(e, elementBounds);
334 if (Double.isNaN(elementBounds.getWidth()) || Double.isNaN(elementBounds.getHeight()))
337 // Get optionally transformed axis-aligned bounds of the element,
338 // expanded by 1mm in each direction.
339 Rectangle2D transformedBounds = transform != null
340 ? toBoundingRectangle(GeometryUtils.transformShape(elementBounds, transform))
342 return org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(transformedBounds, 1e-3);
345 private static boolean pickByOutline(IElement e, PickRequest request, Rectangle2D elementBoundsOnCanvas, AffineTransform canvasToElement, List<Outline> outlineHandlers) {
346 // Rough filtering with bounds
347 if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas))
350 // Convert pick shape to element coordinates
351 AffineTransform elementToCanvas;
353 elementToCanvas = canvasToElement.createInverse();
354 } catch (NoninvertibleTransformException ex) {
355 throw new RuntimeException(ex);
357 Shape pickShapeInElementCoords = GeometryUtils.transformShape(request.pickArea, elementToCanvas);
359 // Intersection with one shape is enough
360 if (request.pickPolicy == PickPolicy.PICK_INTERSECTING_OBJECTS) {
361 for (Outline es : outlineHandlers) {
362 Shape elementShape = es.getElementShape(e);
363 if (elementShape == null)
365 if (GeometryUtils.intersects(pickShapeInElementCoords, elementShape)) {
372 // Contains of all shapes is required
373 if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) {
374 for (Outline es : outlineHandlers) {
375 Shape elementShape = es.getElementShape(e);
376 if (!GeometryUtils.contains(pickShapeInElementCoords, elementShape))
385 private static boolean pickByBounds(IElement e, PickRequest request, Rectangle2D elementBoundsOnCanvas) {
386 if (request.pickPolicy == PickPolicy.PICK_INTERSECTING_OBJECTS) {
387 if (GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas))
389 } else if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) {
390 if (GeometryUtils.contains(request.pickArea, elementBoundsOnCanvas))