]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.g2d/src/org/simantics/g2d/diagram/handler/impl/PickContextImpl.java
Prevent ConcurrentModificationException in StyledRouteGraphRenderer
[simantics/platform.git] / bundles / org.simantics.g2d / src / org / simantics / g2d / diagram / handler / impl / PickContextImpl.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 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  *******************************************************************************/
12 package org.simantics.g2d.diagram.handler.impl;
13
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;
22 import java.util.Map;
23 import java.util.Objects;
24 import java.util.Set;
25 import java.util.function.Predicate;
26 import java.util.stream.Collectors;
27
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;
51
52 /**
53  * @author Toni Kalajainen
54  */
55 public class PickContextImpl implements PickContext {
56
57         public static final PickContextImpl INSTANCE = new PickContextImpl(); 
58
59         private static final Logger LOGGER = LoggerFactory.getLogger(PickContextImpl.class);
60
61         private static final boolean PERF = false;
62
63         private static final ThreadLocal<Rectangle2D> perThreadElementBounds = ThreadLocal.withInitial(() -> new Rectangle2D.Double());
64
65         @Override
66         public void pick(
67                         IDiagram diagram, 
68                         PickRequest request, 
69                         Collection<IElement> finalResult) 
70         {
71                 assert(diagram!=null);
72                 assert(request!=null);
73                 assert(finalResult!=null);
74
75                 long startTime = PERF ? System.nanoTime() : 0L;
76
77                 List<IElement> result = pickElements(diagram, request);
78
79                 if (PERF) {
80                         long endTime = System.nanoTime();
81                         LOGGER.info("[picked " + result.size() + " elements @ " + request.pickArea + "] total pick time : " + ((endTime - startTime)*1e-6));
82                 }
83
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);
89                         } else {
90                                 finalResult.addAll(result);
91                         }
92                 }
93         }
94
95         private static class PickFilter implements Predicate<IElement> {
96
97                 private final PickRequest request;
98                 private final ILayers layers;
99                 private final boolean checkLayers;
100
101                 public PickFilter(PickRequest request, ILayers layers) {
102                         this.request = request;
103                         this.layers = layers;
104                         this.checkLayers = layers != null && !layers.getIgnoreFocusSettings();
105                 }
106
107                 @Override
108                 public boolean test(IElement e) {
109                         // Ignore hidden elements.
110                         if (ElementUtils.isHidden(e))
111                                 return false;
112
113                         ElementClass ec = e.getElementClass();
114
115                         if (checkLayers) {
116                                 ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
117                                 if (el != null && !el.isFocusable(e, layers))
118                                         return false;
119                         }
120
121                         if (request.pickFilter != null && !request.pickFilter.accept(e))
122                                 return false;
123
124                         // Get InternalSize for retrieving bounds, ignore elements that have no bounds
125                         InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class);
126                         if (b == null)
127                                 return false;
128
129                         // Anything that is without a transformation is not pickable
130                         AffineTransform canvasToElement = getTransform(e);
131                         if (canvasToElement == null)
132                                 return false;
133
134                         Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement);
135                         if (elementBoundsOnCanvas == null)
136                                 return false;
137
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());
144                                         return false;
145                                 }
146
147                                 // NOTE: this doesn't support Pick2 interface anymore
148                                 for (Pick p : pickHandlers) {
149                                         if (p.pickTest(e, request.pickArea, request.pickPolicy)) {
150                                                 return true;
151                                         }
152                                 }
153                                 return false;
154                         }
155
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);
160
161                         // Finally, pick by rectangle
162                         return pickByBounds(e, request, elementBoundsOnCanvas);
163                 }
164
165         }
166
167         private static Rectangle2D toBoundingRectangle(Shape shape) {
168                 if (shape instanceof Rectangle2D)
169                         return (Rectangle2D) shape;
170                 else
171                         return shape.getBounds2D();
172         }
173
174         private static List<IElement> pickElements(IDiagram diagram, PickRequest request) {
175                 ILayers layers = diagram.getHint(DiagramHints.KEY_LAYERS);
176
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)
180                                 : null;
181
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. 
186
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<>())
192                                                 .stream()
193                                                 .parallel()
194                                                 .map(nodeToElement::get)
195                                                 .filter(Objects::nonNull)
196                                                 .filter(new PickFilter(request, layers))
197                                                 .collect(Collectors.toList());
198                         } else {
199                                 // Slower version for when DiagramHints.NODE_TO_ELEMENT_MAP is not used
200                                 Set<IG2DNode> nodes =
201                                                 new HashSet<>(
202                                                                 rtree.intersectingNodes(
203                                                                                 toBoundingRectangle(request.pickArea),
204                                                                                 new ArrayList<>())
205                                                                 );
206
207                                 return diagram.getSnapshot().stream()
208                                                 .parallel()
209                                                 // Choose only elements that are under the pick region bounds-wise
210                                                 .filter(e -> {
211                                                         INode node = e.getHint(ElementHints.KEY_SG_NODE);
212                                                         return nodes.contains(node);
213                                                 })
214                                                 // Perform comprehensive picking
215                                                 .filter(new PickFilter(request, layers))
216                                                 .collect(Collectors.toList());
217                         }
218
219                 } else {
220
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();
226
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))
231                                         return;
232
233                                 ElementClass ec = e.getElementClass();
234
235                                 if (checkLayers) {
236                                         ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class);
237                                         if (el != null && !el.isFocusable(e, layers))
238                                                 return;
239                                 }
240
241                                 if (request.pickFilter != null && !request.pickFilter.accept(e))
242                                         return;
243
244                                 // Get InternalSize for retrieving bounds, ignore elements that have no bounds
245                                 InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class);
246                                 if (b == null)
247                                         return;
248
249                                 // Anything that is without a transformation is not pickable
250                                 AffineTransform canvasToElement = getTransform(e);
251                                 if (canvasToElement == null)
252                                         return;
253
254                                 Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement);
255                                 if (elementBoundsOnCanvas == null)
256                                         return;
257
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());
264                                                 return;
265                                         }
266
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)
271                                                                 return;
272                                                 } else {
273                                                         if (p.pickTest(e, request.pickArea, request.pickPolicy)) {
274                                                                 result.add(e);
275                                                                 return;
276                                                         }
277                                                 }
278                                         }
279                                         return;
280                                 }
281
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)) {
286                                                 result.add(e);
287                                         }
288                                         return;
289                                 }
290
291                                 // Finally, pick by bounding rectangle
292                                 if (pickByBounds(e, request, elementBoundsOnCanvas))
293                                         result.add(e);
294                         });
295
296                         return result;
297
298                 }
299         }
300
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)
306                         return null;
307
308                 double det = canvasToElement.getDeterminant();
309                 if (det == 0) {
310                         // Singular transform, only take the translation from it to move on.
311                         canvasToElement = AffineTransform.getTranslateInstance(canvasToElement.getTranslateX(), canvasToElement.getTranslateY());
312                 }
313
314                 return canvasToElement;
315         }
316
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()))
322                         return null;
323
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))
328                                 : elementBounds;
329                 return org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(transformedBounds, 1e-3);
330         }
331
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))
335                         return false;
336
337                 // Convert pick shape to element coordinates
338                 AffineTransform elementToCanvas;
339                 try {
340                         elementToCanvas = canvasToElement.createInverse();
341                 } catch (NoninvertibleTransformException ex) {
342                         throw new RuntimeException(ex);
343                 }
344                 Shape pickShapeInElementCoords = GeometryUtils.transformShape(request.pickArea, elementToCanvas);
345
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)
351                                         return false;
352                                 if (GeometryUtils.intersects(pickShapeInElementCoords, elementShape)) {
353                                         return true;
354                                 }
355                         }
356                         return false;
357                 }
358
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))
364                                         return false;
365                         }
366                         return true;
367                 }
368
369                 return false;
370         }
371
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))
375                                 return true;
376                 } else if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) {
377                         if (GeometryUtils.contains(request.pickArea, elementBoundsOnCanvas))
378                                 return true;
379                 }
380                 return false;
381         }
382
383 }