Bug #12502 fixed: Rotate, flip did not work on ports
[scilab.git] / scilab / modules / xcos / src / java / org / scilab / modules / xcos / utils / BlockPositioning.java
1 /*
2  * Scilab ( http://www.scilab.org/ ) - This file is part of Scilab
3  * Copyright (C) 2009 - DIGITEO - Antoine ELIAS
4  * Copyright (C) 2011-2015 - Scilab Enterprises - Clement DAVID
5  * Copyright (C) 2018 - ESI Group - Clement DAVID
6  *
7  * Copyright (C) 2012 - 2016 - Scilab Enterprises
8  *
9  * This file is hereby licensed under the terms of the GNU GPL v2.0,
10  * pursuant to article 5.3.4 of the CeCILL v.2.1.
11  * This file was originally licensed under the terms of the CeCILL v2.1,
12  * and continues to be available under such terms.
13  * For more information, see the COPYING file which you should have received
14  * along with this program.
15  *
16  */
17
18 package org.scilab.modules.xcos.utils;
19
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.List;
23 import java.util.Map;
24
25 import org.scilab.modules.xcos.JavaController;
26 import org.scilab.modules.xcos.Kind;
27 import org.scilab.modules.xcos.ObjectProperties;
28 import org.scilab.modules.xcos.block.BasicBlock;
29 import org.scilab.modules.xcos.graph.XcosDiagram;
30 import org.scilab.modules.xcos.io.scicos.BasicBlockInfo;
31 import org.scilab.modules.xcos.port.BasicPort;
32 import org.scilab.modules.xcos.port.Orientation;
33
34 import com.mxgraph.model.mxGeometry;
35 import com.mxgraph.model.mxIGraphModel;
36 import com.mxgraph.util.mxStyleUtils;
37 import org.scilab.modules.graph.utils.StyleMap;
38
39 /**
40  * Helpers to place port on a block.
41  */
42 public final class BlockPositioning {
43
44     /**
45      * The default grid size. This value is used when the grid size isn't accessible (on the palette).
46      */
47     public static final double DEFAULT_GRIDSIZE = Double.MIN_NORMAL;
48     /** The rotation step of the clockwise and anticlockwise rotation */
49     public static final int ROTATION_STEP = 90;
50     /** The max valid rotation value (always 360 degres) */
51     public static final int MAX_ROTATION = 360;
52
53     /** This class is a static singleton, thus it must not be instantiated */
54     private BlockPositioning() {
55     }
56
57     /**
58      * Dispatch ports on Block's _WEST_ side.
59      *
60      * @param block
61      *            The block we have to work on.
62      * @param ports
63      *            The ports we have to move on the side.
64      */
65     public static void updateWestPortsPosition(final XcosDiagram diag, BasicBlock block, List<? extends BasicPort> ports) {
66
67         double gridSize = diag.getGridSize();
68
69         final mxGeometry blockGeom = block.getGeometry();
70         assert blockGeom != null;
71         final int portsSize = ports.size();
72         final double blockLength = blockGeom.getHeight();
73         final double segLength = blockLength / (portsSize + 1);
74
75         diag.getModel().beginUpdate();
76         for (int i = 0; i < portsSize; ++i) {
77             final BasicPort port = (ports.get(i));
78             final mxGeometry portGeom = (mxGeometry) port.getGeometry().clone();
79
80             double nonVariantPosition = -portGeom.getWidth();
81             final int order = i;
82             double alignedPosition = calculateAlignedPosition(gridSize, segLength, order);
83
84             portGeom.setX(nonVariantPosition);
85             portGeom.setY(alignedPosition);
86
87             diag.getModel().setGeometry(port, portGeom);
88             diag.getModel().setStyle(port, port.computeLabelPosition(Orientation.WEST));
89         }
90         diag.getModel().endUpdate();
91     }
92
93     /**
94      * Calculate an aligned port position.
95      *
96      * @param gridSize
97      *            the grid size
98      * @param segLength
99      *            the side length
100      * @param i
101      *            the current working index
102      * @return the aligned position on the grid.
103      */
104     private static double calculateAlignedPosition(final double gridSize, final double segLength, int i) {
105         /*
106          * The base position is the origin of the port geometry. It is the upper-left corner position.
107          */
108         final double basePosition = (i + 1) * segLength;
109
110         /*
111          * The aligned base position is the base position aligned on the grid.
112          */
113         final double alignedBasePosition = basePosition - Math.IEEEremainder(basePosition, gridSize);
114
115         /*
116          * The aligned position is the base position translated from origin to the middle of the port.
117          */
118         final double alignedPosition = alignedBasePosition - (BasicPort.DEFAULT_PORTSIZE / 2.0);
119
120         return alignedPosition;
121     }
122
123     /**
124      * Dispatch ports on Block's _NORTH_ side.
125      *
126      * @param block
127      *            The block we have to work on.
128      * @param ports
129      *            The ports we have to move on the side.
130      */
131     public static void updateNorthPortsPosition(final XcosDiagram diag, BasicBlock block, List<? extends BasicPort> ports) {
132         double gridSize = diag.getGridSize();
133
134         final mxGeometry blockGeom = block.getGeometry();
135         assert blockGeom != null;
136         final int portsSize = ports.size();
137         final double blockLength = blockGeom.getWidth();
138         final double segLength = blockLength / (portsSize + 1);
139
140         diag.getModel().beginUpdate();
141         for (int i = 0; i < portsSize; ++i) {
142             final BasicPort port = (ports.get(i));
143             final mxGeometry portGeom = (mxGeometry) port.getGeometry().clone();
144
145             double nonVariantPosition = -portGeom.getHeight();
146             final int order = i;
147             double alignedPosition = calculateAlignedPosition(gridSize, segLength, order);
148
149             portGeom.setX(alignedPosition);
150             portGeom.setY(nonVariantPosition);
151
152             diag.getModel().setGeometry(port, portGeom);
153             diag.getModel().setStyle(port, port.computeLabelPosition(Orientation.NORTH));
154         }
155         diag.getModel().endUpdate();
156     }
157
158     /**
159      * Dispatch ports on Block's _EAST_ side.
160      *
161      * @param block
162      *            The block we have to work on.
163      * @param ports
164      *            The ports we have to move on the side.
165      */
166     public static void updateEastPortsPosition(final XcosDiagram diag, BasicBlock block, List<? extends BasicPort> ports) {
167         double gridSize = diag.getGridSize();
168
169         final mxGeometry blockGeom = block.getGeometry();
170         assert blockGeom != null;
171         final int portsSize = ports.size();
172         final double blockLength = blockGeom.getHeight();
173         final double segLength = blockLength / (portsSize + 1);
174
175         diag.getModel().beginUpdate();
176         for (int i = 0; i < portsSize; ++i) {
177             final BasicPort port = (ports.get(i));
178             final mxGeometry portGeom = (mxGeometry) port.getGeometry().clone();
179
180             double nonVariantPosition = blockGeom.getWidth();
181             final int order = i;
182             double alignedPosition = calculateAlignedPosition(gridSize, segLength, order);
183
184             portGeom.setX(nonVariantPosition);
185             portGeom.setY(alignedPosition);
186
187             diag.getModel().setGeometry(port, portGeom);
188             diag.getModel().setStyle(port, port.computeLabelPosition(Orientation.EAST));
189         }
190         diag.getModel().endUpdate();
191     }
192
193     /**
194      * Dispatch ports on Block's _SOUTH_ side.
195      *
196      * @param block
197      *            The block we have to work on.
198      * @param ports
199      *            The ports we have to move on the side.
200      */
201     public static void updateSouthPortsPosition(final XcosDiagram diag, BasicBlock block, List<? extends BasicPort> ports) {
202         double gridSize = diag.getGridSize();
203
204         final mxGeometry blockGeom = block.getGeometry();
205         assert blockGeom != null;
206         final int portsSize = ports.size();
207         final double blockLength = blockGeom.getWidth();
208         final double segLength = blockLength / (portsSize + 1);
209
210         diag.getModel().beginUpdate();
211         for (int i = 0; i < portsSize; ++i) {
212             final BasicPort port = (ports.get(i));
213             final mxGeometry portGeom = (mxGeometry) port.getGeometry().clone();
214
215             double nonVariantPosition = blockGeom.getHeight();
216             final int order = i;
217             double alignedPosition = calculateAlignedPosition(gridSize, segLength, order);
218
219             portGeom.setX(alignedPosition);
220             portGeom.setY(nonVariantPosition);
221
222             diag.getModel().setGeometry(port, portGeom);
223             diag.getModel().setStyle(port, port.computeLabelPosition(Orientation.SOUTH));
224         }
225         diag.getModel().endUpdate();
226     }
227
228     /**
229      * Update all the port position of the block.
230      *
231      * @param block
232      *            The block we have to work on.
233      */
234     public static void updatePortsPosition(final XcosDiagram diag, BasicBlock block) {
235         final Map<Orientation, List<BasicPort>> ports = BasicBlockInfo.getAllOrientedPorts(block);
236
237         diag.getModel().beginUpdate();
238         for (Orientation iter : Orientation.values()) {
239             List<BasicPort> orientedPorts = ports.get(iter);
240             if (orientedPorts != null && !orientedPorts.isEmpty()) {
241                 updatePortsPositions(diag, block, orientedPorts, iter);
242             }
243         }
244         diag.getModel().endUpdate();
245     }
246
247     /**
248      * Update the port position for the specified orientation. This function manage the flip and mirror properties.
249      *
250      * @param block
251      *            The block we are working on
252      * @param ports
253      *            The ports at with the specified orientation
254      * @param iter
255      *            The orientation.
256      */
257     private static void updatePortsPositions(final XcosDiagram diag, BasicBlock block, List<BasicPort> ports, Orientation iter) {
258         @SuppressWarnings("serial")
259         JavaController controller = new JavaController();
260         String[] style = new String[1];
261         controller.getObjectProperty(block.getUID(), Kind.BLOCK, ObjectProperties.STYLE, style);
262         StyleMap styleMap = new StyleMap(style[0]);
263
264         final boolean mirrored = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_MIRROR));
265         final boolean flipped = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_FLIP));
266         final int intRotation = Double.valueOf(styleMap.getOrDefault(XcosConstants.STYLE_ROTATION, "0")).intValue();
267         final int angle = (intRotation % 360 + 360) % 360;
268
269         List<BasicPort> working = new ArrayList<>(ports);
270
271         /* List order modification for opposite angle */
272         if (0 < angle && angle < 270) {
273             Collections.reverse(working);
274         }
275
276         /* List order modification with the flip flag */
277         if (flipped) {
278             if (iter == Orientation.EAST || iter == Orientation.WEST) {
279                 Collections.reverse(working);
280             }
281         }
282
283         /* List order modification with the mirror flag */
284         if (mirrored) {
285             if (iter == Orientation.NORTH || iter == Orientation.SOUTH) {
286                 Collections.reverse(working);
287             }
288         }
289
290         /*
291          * Ugly modification of the iter to update at the right position Works only for 0 - 90 - 180 - 270 angles.
292          */
293         Orientation rotated = rotateOrientation(iter, mirrored, flipped);
294
295         updatePortsPosition(diag, block, rotated, angle, working);
296     }
297
298     /**
299      * Ugly modification of the iter to update at the right position. Works only for 0 - 90 - 180 - 270 angles.
300      *
301      * @param iter
302      *            the real orientation
303      * @param mirrored
304      *            is the block mirrored
305      * @param flipped
306      *            is the block flipped
307      * @return The modified orientation
308      */
309     private static Orientation rotateOrientation(final Orientation iter, final boolean mirrored, final boolean flipped) {
310         final int nbOfOrientations = Orientation.values().length; // 4
311         Orientation rotated = iter;
312
313         /* Flip & Mirror management */
314         if (mirrored) {
315             if (rotated == Orientation.EAST || rotated == Orientation.WEST) {
316                 rotated = Orientation.values()[(rotated.ordinal() + 2) % nbOfOrientations];
317             }
318         }
319         if (flipped) {
320             if (rotated == Orientation.NORTH || rotated == Orientation.SOUTH) {
321                 rotated = Orientation.values()[(rotated.ordinal() + 2) % nbOfOrientations];
322             }
323         }
324         return rotated;
325     }
326
327     /**
328      * Update the ports positions according to the angle. This function doesn't handle order inversion.
329      *
330      * @param block
331      *            The block we are working on
332      * @param iter
333      *            The current port orientation
334      * @param angle
335      *            The angle we have to rotate
336      * @param working
337      *            The ordered ports we are working on.
338      */
339     private static void updatePortsPosition(final XcosDiagram diag, BasicBlock block, Orientation iter, final double angle, List<BasicPort> working) {
340         /*
341          * Ugly modification of the iter to update at the right position Works only for 0 - 90 - 180 - 270 angles.
342          */
343         final int nbOfOrientations = Orientation.values().length; // 4
344         Orientation rotated = iter;
345
346         /* Angle management */
347         double rotationIndex = angle / ROTATION_STEP;
348         rotated = Orientation.values()[(rotated.ordinal() + (int) rotationIndex) % nbOfOrientations];
349
350         /* Call the associated function */
351         switch (rotated) {
352             case NORTH:
353                 updateNorthPortsPosition(diag, block, working);
354                 break;
355             case SOUTH:
356                 updateSouthPortsPosition(diag, block, working);
357                 break;
358             case EAST:
359                 updateEastPortsPosition(diag, block, working);
360                 break;
361             case WEST:
362                 updateWestPortsPosition(diag, block, working);
363                 break;
364
365             default:
366                 break;
367         }
368     }
369
370     /**
371      * Rotate all the port of the block.
372      *
373      * @param block
374      *            The block to work on.
375      */
376     public static void rotateAllPorts(final XcosDiagram diag, BasicBlock block) {
377         JavaController controller = new JavaController();
378         String[] style = new String[1];
379         controller.getObjectProperty(block.getUID(), Kind.BLOCK, ObjectProperties.STYLE, style);
380         StyleMap styleMap = new StyleMap(style[0]);
381
382         final boolean mirrored = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_MIRROR));
383         final boolean flipped = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_FLIP));
384         final int intRotation = Double.valueOf(styleMap.getOrDefault(XcosConstants.STYLE_ROTATION, "0")).intValue();
385         final int angle = (intRotation % 360 + 360) % 360;
386
387         final int childrenCount = block.getChildCount();
388         for (int i = 0; i < childrenCount; ++i) {
389             if (block.getChildAt(i) instanceof BasicPort) {
390                 final BasicPort port = (BasicPort) block.getChildAt(i);
391                 final Orientation orientation = port.getOrientation();
392
393                 diag.getModel().beginUpdate();
394
395                 /* Apply angle */
396                 final mxIGraphModel model = diag.getModel();
397                 final String rot = Integer.toString(orientation.getRelativeAngle(angle, port.getClass(), flipped, mirrored));
398                 mxStyleUtils.setCellStyles(model, new Object[] { port }, XcosConstants.STYLE_ROTATION, rot);
399
400                 diag.getModel().endUpdate();
401             }
402         }
403     }
404
405     /**
406      * Update the geometry of the block's ports.
407      *
408      * @param block
409      *            The block to work on
410      */
411     public static void updateBlockView(final XcosDiagram diag, BasicBlock block) {
412         if (block.getKind() != Kind.BLOCK) {
413             return;
414         }
415
416         diag.getModel().beginUpdate();
417         block.updateBlockView();
418         updatePortsPosition(diag, block);
419         rotateAllPorts(diag, block);
420         diag.getModel().endUpdate();
421
422         /*
423          * TODO: bug #6705; This placement trick doesn't work on the first block Dnd as the view is not revalidated.
424          *
425          * On block loading, parentDiagram is null thus placement is not performed.
426          */
427     }
428
429     /**
430      * Update the geometry of the block's ports.
431      * @param diag XcosDiagram
432      * @param blocks array with several blocks
433      */
434     public static void updatePortsPosition(final XcosDiagram diag, Object[] blocks) {
435         diag.getModel().beginUpdate();
436         for (Object b : blocks) {
437             BasicBlock block = (BasicBlock) b;
438             updatePortsPosition(diag, block);
439         }
440         diag.getModel().endUpdate();
441     }
442
443     /**
444      * Flip a block (horizontal inversion).
445      *
446      * @param block
447      *            The block to work on
448      */
449     public static void toggleFlip(final XcosDiagram diag, BasicBlock block) {
450         StyleMap styleMap = new StyleMap(block.getStyle());
451         final boolean invertedFlip = ! Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_FLIP));
452
453         styleMap.put(XcosConstants.STYLE_FLIP, Boolean.toString(invertedFlip));
454         cleanup(styleMap);
455         
456         diag.getModel().setStyle(block, styleMap.toString());
457         updateBlockView(diag, block);
458     }
459
460     /**
461      * Mirror a block (vertical inversion).
462      *
463      * @param block
464      *            The block to work on
465      */
466     public static void toggleMirror(final XcosDiagram diag, BasicBlock block) {
467         StyleMap styleMap = new StyleMap(block.getStyle());
468         final boolean invertedFlip = ! Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_MIRROR));
469
470         styleMap.put(XcosConstants.STYLE_MIRROR, Boolean.toString(invertedFlip));
471         cleanup(styleMap);
472         
473         diag.getModel().setStyle(block, styleMap.toString());
474         updateBlockView(diag, block);
475     }
476
477     /**
478      * Rotate a block with an anti-clockwise next value
479      *
480      * @param block
481      *            The block to work on
482      */
483     public static void toggleAntiClockwiseRotation(final XcosDiagram diag, BasicBlock block) {
484         StyleMap styleMap = new StyleMap(block.getStyle());
485         computeNextAntiClockwiseAngle(styleMap);
486         cleanup(styleMap);
487
488         mxGeometry old = block.getGeometry();
489         mxGeometry rotated = new mxGeometry(old.getCenterX() - old.getHeight() / 2,
490                                             old.getCenterY() - old.getWidth() / 2, old.getHeight(), old.getWidth());
491         diag.getModel().setGeometry(block, rotated);
492         diag.getModel().setStyle(block, styleMap.toString());
493         updateBlockView(diag, block);
494     }
495
496     /**
497      * Compute and set some properties to rotate the element style
498      * @param styleMap
499      *            the data to parse
500      */
501     private static void computeNextAntiClockwiseAngle(StyleMap styleMap) {
502         final int rotation = Double.valueOf(styleMap.getOrDefault(XcosConstants.STYLE_ROTATION, "0")).intValue();
503         
504         int angle = roundAngle(rotation - ROTATION_STEP);
505         if (angle == 0) {
506             styleMap.remove(XcosConstants.STYLE_ROTATION);
507         } else {
508             styleMap.put(XcosConstants.STYLE_ROTATION, Integer.toString(angle));
509         }
510     }
511
512     /**
513      * Convert any angle value to a valid block value
514      *
515      * @param angle
516      *            the non valid value
517      * @return the nearest graph valid value
518      */
519     public static int roundAngle(int angle) {
520         int ret = angle;
521         if (angle < 0 || angle > MAX_ROTATION) {
522             ret = (angle + MAX_ROTATION) % MAX_ROTATION;
523         }
524
525         for (int i = 0; i < (MAX_ROTATION / ROTATION_STEP); i++) {
526             int min = i * ROTATION_STEP;
527             int max = (i + 1) * ROTATION_STEP;
528
529             if (ret < (min + max) / 2) {
530                 ret = min;
531                 break;
532             }
533         }
534         return ret;
535     }
536     
537     private static void cleanup(StyleMap styleMap) {
538         String rotation = styleMap.get(XcosConstants.STYLE_ROTATION);
539         if ("0".equals(rotation))
540             styleMap.remove(XcosConstants.STYLE_ROTATION);
541         String flip = styleMap.get(XcosConstants.STYLE_FLIP);
542         if ("false".equals(flip))
543             styleMap.remove(XcosConstants.STYLE_FLIP);
544         String mirror = styleMap.get(XcosConstants.STYLE_MIRROR);
545         if ("false".equals(mirror))
546             styleMap.remove(XcosConstants.STYLE_MIRROR);
547     }
548 }