]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.g2d/src/org/simantics/g2d/diagram/handler/impl/PickContextImpl.java
Allow PickSorter access to the PickRequest used for pick operation
[simantics/platform.git] / bundles / org.simantics.g2d / src / org / simantics / g2d / diagram / handler / impl / PickContextImpl.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2020 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
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;
14
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;
23 import java.util.Map;
24 import java.util.Objects;
25 import java.util.Set;
26 import java.util.function.Predicate;
27 import java.util.stream.Collectors;
28
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;
52
53 /**
54  * @author Toni Kalajainen
55  * @author Jani Simomaa
56  * @author Tuukka Lehtonen
57  */
58 public class PickContextImpl implements PickContext {
59
60         public static final PickContextImpl INSTANCE = new PickContextImpl(); 
61
62         private static final Logger LOGGER = LoggerFactory.getLogger(PickContextImpl.class);
63
64         private static final boolean PERF = false;
65
66         private static final ThreadLocal<Rectangle2D> perThreadElementBounds = ThreadLocal.withInitial(() -> new Rectangle2D.Double());
67
68         private boolean useRTree;
69
70         public PickContextImpl() {
71                 this(false);
72         }
73
74         public PickContextImpl(boolean useRTree) {
75                 this.useRTree = useRTree;
76         }
77
78         @Override
79         public void pick(
80                         IDiagram diagram, 
81                         PickRequest request, 
82                         Collection<IElement> finalResult) 
83         {
84                 assert(diagram!=null);
85                 assert(request!=null);
86                 assert(finalResult!=null);
87
88                 long startTime = PERF ? System.nanoTime() : 0L;
89
90                 List<IElement> result = pickElements(diagram, request);
91
92                 if (PERF) {
93                         long endTime = System.nanoTime();
94                         LOGGER.info("[picked {} elements @ {}] total pick time {} ms", result.size(), request.pickArea, ((endTime - startTime)*1e-6));
95                 }
96
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);
102                         } else {
103                                 finalResult.addAll(result);
104                         }
105                 }
106         }
107
108         private static class PickFilter implements Predicate<IElement> {
109
110                 private final PickRequest request;
111                 private final ILayers layers;
112                 private final boolean checkLayers;
113
114                 public PickFilter(PickRequest request, ILayers layers) {
115                         this.request = request;
116                         this.layers = layers;
117                         this.checkLayers = layers != null && !layers.getIgnoreFocusSettings();
118                 }
119
120                 @Override
121                 public boolean test(IElement e) {
122                         // Ignore hidden elements.
123                         if (ElementUtils.isHidden(e))
124                                 return false;
125
126                         ElementClass ec = e.getElementClass();
127
128                         if (checkLayers) {
129                                 ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
130                                 if (el != null && !el.isFocusable(e, layers))
131                                         return false;
132                         }
133
134                         if (request.pickFilter != null && !request.pickFilter.accept(e))
135                                 return false;
136
137                         // Get InternalSize for retrieving bounds, ignore elements that have no bounds
138                         InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class);
139                         if (b == null)
140                                 return false;
141
142                         // Anything that is without a transformation is not pickable
143                         AffineTransform canvasToElement = getTransform(e);
144                         if (canvasToElement == null)
145                                 return false;
146
147                         Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement);
148                         if (elementBoundsOnCanvas == null)
149                                 return false;
150
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());
157                                         return false;
158                                 }
159
160                                 // NOTE: this doesn't support Pick2 interface anymore
161                                 for (Pick p : pickHandlers) {
162                                         if (p.pickTest(e, request.pickArea, request.pickPolicy)) {
163                                                 return true;
164                                         }
165                                 }
166                                 return false;
167                         }
168
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);
173
174                         // Finally, pick by rectangle
175                         return pickByBounds(e, request, elementBoundsOnCanvas);
176                 }
177
178         }
179
180         private static Rectangle2D toBoundingRectangle(Shape shape) {
181                 if (shape instanceof Rectangle2D)
182                         return (Rectangle2D) shape;
183                 else
184                         return shape.getBounds2D();
185         }
186
187         private List<IElement> pickElements(IDiagram diagram, PickRequest request) {
188                 ILayers layers = diagram.getHint(DiagramHints.KEY_LAYERS);
189
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)
193                                 : null;
194
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. 
199
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<>())
205                                                 .stream()
206                                                 .parallel()
207                                                 .map(nodeToElement::get)
208                                                 .filter(Objects::nonNull)
209                                                 .filter(new PickFilter(request, layers))
210                                                 .collect(Collectors.toList());
211                         } else {
212                                 // Slower version for when DiagramHints.NODE_TO_ELEMENT_MAP is not used
213                                 Set<IG2DNode> nodes =
214                                                 new HashSet<>(
215                                                                 rtree.intersectingNodes(
216                                                                                 toBoundingRectangle(request.pickArea),
217                                                                                 new ArrayList<>())
218                                                                 );
219
220                                 return diagram.getSnapshot().stream()
221                                                 .parallel()
222                                                 // Choose only elements that are under the pick region bounds-wise
223                                                 .filter(e -> {
224                                                         INode node = e.getHint(ElementHints.KEY_SG_NODE);
225                                                         return nodes.contains(node);
226                                                 })
227                                                 // Perform comprehensive picking
228                                                 .filter(new PickFilter(request, layers))
229                                                 .collect(Collectors.toList());
230                         }
231
232                 } else {
233
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();
239
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))
244                                         return;
245
246                                 ElementClass ec = e.getElementClass();
247
248                                 if (checkLayers) {
249                                         ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
250                                         if (el != null && !el.isFocusable(e, layers))
251                                                 return;
252                                 }
253
254                                 if (request.pickFilter != null && !request.pickFilter.accept(e))
255                                         return;
256
257                                 // Get InternalSize for retrieving bounds, ignore elements that have no bounds
258                                 InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class);
259                                 if (b == null)
260                                         return;
261
262                                 // Anything that is without a transformation is not pickable
263                                 AffineTransform canvasToElement = getTransform(e);
264                                 if (canvasToElement == null)
265                                         return;
266
267                                 Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement);
268                                 if (elementBoundsOnCanvas == null)
269                                         return;
270
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());
277                                                 return;
278                                         }
279
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)
284                                                                 return;
285                                                 } else {
286                                                         if (p.pickTest(e, request.pickArea, request.pickPolicy)) {
287                                                                 result.add(e);
288                                                                 return;
289                                                         }
290                                                 }
291                                         }
292                                         return;
293                                 }
294
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)) {
299                                                 result.add(e);
300                                         }
301                                         return;
302                                 }
303
304                                 // Finally, pick by bounding rectangle
305                                 if (pickByBounds(e, request, elementBoundsOnCanvas))
306                                         result.add(e);
307                         });
308
309                         return result;
310
311                 }
312         }
313
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)
319                         return null;
320
321                 double det = canvasToElement.getDeterminant();
322                 if (det == 0) {
323                         // Singular transform, only take the translation from it to move on.
324                         canvasToElement = AffineTransform.getTranslateInstance(canvasToElement.getTranslateX(), canvasToElement.getTranslateY());
325                 }
326
327                 return canvasToElement;
328         }
329
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()))
335                         return null;
336
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))
341                                 : elementBounds;
342                 return org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(transformedBounds, 1e-3);
343         }
344
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))
348                         return false;
349
350                 // Convert pick shape to element coordinates
351                 AffineTransform elementToCanvas;
352                 try {
353                         elementToCanvas = canvasToElement.createInverse();
354                 } catch (NoninvertibleTransformException ex) {
355                         throw new RuntimeException(ex);
356                 }
357                 Shape pickShapeInElementCoords = GeometryUtils.transformShape(request.pickArea, elementToCanvas);
358
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)
364                                         return false;
365                                 if (GeometryUtils.intersects(pickShapeInElementCoords, elementShape)) {
366                                         return true;
367                                 }
368                         }
369                         return false;
370                 }
371
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))
377                                         return false;
378                         }
379                         return true;
380                 }
381
382                 return false;
383         }
384
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))
388                                 return true;
389                 } else if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) {
390                         if (GeometryUtils.contains(request.pickArea, elementBoundsOnCanvas))
391                                 return true;
392                 }
393                 return false;
394         }
395
396 }