b9ef583e2e88e83e7dcd782b75d55328dd229347
[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         final List<BasicPort> invertedPorts = new ArrayList<BasicPort>(ports) {
260             {
261                 Collections.reverse(this);
262             }
263         };
264
265         JavaController controller = new JavaController();
266         String[] style = new String[1];
267         controller.getObjectProperty(block.getUID(), Kind.BLOCK, ObjectProperties.STYLE, style);
268         StyleMap styleMap = new StyleMap(style[0]);
269
270         final boolean mirrored = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_MIRROR));
271         final boolean flipped = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_FLIP));
272         final int intRotation = Double.valueOf(styleMap.getOrDefault(XcosConstants.STYLE_ROTATION, "0")).intValue();
273         final int angle = (intRotation % 360 + 360) % 360;
274
275         List<BasicPort> working = ports;
276
277         /* List order modification with the flip flag */
278         if (flipped) {
279             if (iter == Orientation.NORTH || iter == Orientation.SOUTH) {
280                 working = invertedPorts;
281             }
282         }
283
284         /* List order modification with the mirror flag */
285         if (mirrored) {
286             if (iter == Orientation.EAST || iter == Orientation.WEST) {
287                 working = invertedPorts;
288             }
289         }
290
291         /*
292          * Ugly modification of the iter to update at the right position Works only for 0 - 90 - 180 - 270 angles.
293          */
294         Orientation rotated = rotateOrientation(iter, mirrored, flipped);
295
296         updatePortsPosition(diag, block, rotated, angle, working);
297     }
298
299     /**
300      * Ugly modification of the iter to update at the right position. Works only for 0 - 90 - 180 - 270 angles.
301      *
302      * @param iter
303      *            the real orientation
304      * @param mirrored
305      *            is the block mirrored
306      * @param flipped
307      *            is the block flipped
308      * @return The modified orientation
309      */
310     private static Orientation rotateOrientation(final Orientation iter, final boolean mirrored, final boolean flipped) {
311         final int nbOfOrientations = Orientation.values().length; // 4
312         Orientation rotated = iter;
313
314         /* Flip & Mirror management */
315         if (mirrored) {
316             if (rotated == Orientation.EAST || rotated == Orientation.WEST) {
317                 rotated = Orientation.values()[(rotated.ordinal() + 2) % nbOfOrientations];
318             }
319         }
320         if (flipped) {
321             if (rotated == Orientation.NORTH || rotated == Orientation.SOUTH) {
322                 rotated = Orientation.values()[(rotated.ordinal() + 2) % nbOfOrientations];
323             }
324         }
325         return rotated;
326     }
327
328     /**
329      * Update the ports positions according to the angle. This function doesn't handle order inversion.
330      *
331      * @param block
332      *            The block we are working on
333      * @param iter
334      *            The current port orientation
335      * @param angle
336      *            The angle we have to rotate
337      * @param working
338      *            The ordered ports we are working on.
339      */
340     private static void updatePortsPosition(final XcosDiagram diag, BasicBlock block, Orientation iter, final double angle, List<BasicPort> working) {
341         /*
342          * Ugly modification of the iter to update at the right position Works only for 0 - 90 - 180 - 270 angles.
343          */
344         final int nbOfOrientations = Orientation.values().length; // 4
345         Orientation rotated = iter;
346
347         /* Angle management */
348         double rotationIndex = angle / ROTATION_STEP;
349         rotated = Orientation.values()[(rotated.ordinal() + (int) rotationIndex) % nbOfOrientations];
350
351         /* Call the associated function */
352         switch (rotated) {
353             case NORTH:
354                 updateNorthPortsPosition(diag, block, working);
355                 break;
356             case SOUTH:
357                 updateSouthPortsPosition(diag, block, working);
358                 break;
359             case EAST:
360                 updateEastPortsPosition(diag, block, working);
361                 break;
362             case WEST:
363                 updateWestPortsPosition(diag, block, working);
364                 break;
365
366             default:
367                 break;
368         }
369     }
370
371     /**
372      * Rotate all the port of the block.
373      *
374      * @param block
375      *            The block to work on.
376      */
377     public static void rotateAllPorts(final XcosDiagram diag, BasicBlock block) {
378         JavaController controller = new JavaController();
379         String[] style = new String[1];
380         controller.getObjectProperty(block.getUID(), Kind.BLOCK, ObjectProperties.STYLE, style);
381         StyleMap styleMap = new StyleMap(style[0]);
382
383         final boolean mirrored = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_MIRROR));
384         final boolean flipped = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_FLIP));
385         final int intRotation = Double.valueOf(styleMap.getOrDefault(XcosConstants.STYLE_ROTATION, "0")).intValue();
386         final int angle = (intRotation % 360 + 360) % 360;
387
388         final int childrenCount = block.getChildCount();
389         for (int i = 0; i < childrenCount; ++i) {
390             if (block.getChildAt(i) instanceof BasicPort) {
391                 final BasicPort port = (BasicPort) block.getChildAt(i);
392                 final Orientation orientation = port.getOrientation();
393
394                 diag.getModel().beginUpdate();
395
396                 /* Apply angle */
397                 final mxIGraphModel model = diag.getModel();
398                 final String rot = Integer.toString(orientation.getRelativeAngle(angle, port.getClass(), flipped, mirrored));
399                 mxStyleUtils.setCellStyles(model, new Object[] { port }, XcosConstants.STYLE_ROTATION, rot);
400
401                 diag.getModel().endUpdate();
402             }
403         }
404     }
405
406     /**
407      * Update the geometry of the block's ports.
408      *
409      * @param block
410      *            The block to work on
411      */
412     public static void updateBlockView(final XcosDiagram diag, BasicBlock block) {
413         if (block.getKind() != Kind.BLOCK) {
414             return;
415         }
416
417         diag.getModel().beginUpdate();
418         block.updateBlockView();
419         updatePortsPosition(diag, block);
420         rotateAllPorts(diag, block);
421         diag.getModel().endUpdate();
422
423         /*
424          * TODO: bug #6705; This placement trick doesn't work on the first block Dnd as the view is not revalidated.
425          *
426          * On block loading, parentDiagram is null thus placement is not performed.
427          */
428     }
429
430     /**
431      * Update the geometry of the block's ports.
432      * @param diag XcosDiagram
433      * @param blocks array with several blocks
434      */
435     public static void updatePortsPosition(final XcosDiagram diag, Object[] blocks) {
436         diag.getModel().beginUpdate();
437         for (Object b : blocks) {
438             BasicBlock block = (BasicBlock) b;
439             updatePortsPosition(diag, block);
440         }
441         diag.getModel().endUpdate();
442     }
443
444     /**
445      * Flip a block (horizontal inversion).
446      *
447      * @param block
448      *            The block to work on
449      */
450     public static void toggleFlip(final XcosDiagram diag, BasicBlock block) {
451         StyleMap styleMap = new StyleMap(block.getStyle());
452         final boolean invertedFlip = ! Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_FLIP));
453
454         styleMap.put(XcosConstants.STYLE_FLIP, Boolean.toString(invertedFlip));
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
472         diag.getModel().setStyle(block, styleMap.toString());
473         updateBlockView(diag, block);
474     }
475
476     /**
477      * Rotate a block with an anti-clockwise next value
478      *
479      * @param block
480      *            The block to work on
481      */
482     public static void toggleAntiClockwiseRotation(final XcosDiagram diag, BasicBlock block) {
483         StyleMap styleMap = new StyleMap(block.getStyle());
484         computeNextAntiClockwiseAngle(styleMap);
485
486         mxGeometry old = block.getGeometry();
487         mxGeometry rotated = new mxGeometry(old.getCenterX() - old.getHeight() / 2,
488                                             old.getCenterY() - old.getWidth() / 2, old.getHeight(), old.getWidth());
489         diag.getModel().setGeometry(block, rotated);
490         diag.getModel().setStyle(block, styleMap.toString());
491         updateBlockView(diag, block);
492     }
493
494     /**
495      * Compute and set some properties to rotate the element style
496      * @param styleMap
497      *            the data to parse
498      */
499     private static void computeNextAntiClockwiseAngle(StyleMap styleMap) {
500         final int rotation = Double.valueOf(styleMap.getOrDefault(XcosConstants.STYLE_ROTATION, "0")).intValue();
501         boolean flip = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_FLIP));
502         boolean mirror = Boolean.TRUE.toString().equals(styleMap.get(XcosConstants.STYLE_MIRROR));
503
504         int angle = (rotation - ROTATION_STEP + MAX_ROTATION) % MAX_ROTATION;
505         if (angle > 90) {
506             angle = angle - 180;
507             flip = !flip;
508             mirror = !mirror;
509         }
510
511         if (angle == 0) {
512             styleMap.remove(XcosConstants.STYLE_ROTATION);
513         } else {
514             styleMap.put(XcosConstants.STYLE_ROTATION, Integer.toString(angle));
515         }
516         if (!flip) {
517             styleMap.remove(XcosConstants.STYLE_FLIP);
518         } else {
519             styleMap.put(XcosConstants.STYLE_FLIP, Boolean.toString(flip));
520         }
521         if (!mirror) {
522             styleMap.remove(XcosConstants.STYLE_MIRROR);
523         } else {
524             styleMap.put(XcosConstants.STYLE_MIRROR, Boolean.toString(mirror));
525         }
526     }
527
528     /**
529      * Convert any angle value to a valid block value
530      *
531      * @param angle
532      *            the non valid value
533      * @return the nearest graph valid value
534      */
535     public static int roundAngle(int angle) {
536         int ret = angle;
537         if (angle < 0 || angle > MAX_ROTATION) {
538             ret = (angle + MAX_ROTATION) % MAX_ROTATION;
539         }
540
541         for (int i = 0; i < (MAX_ROTATION / ROTATION_STEP); i++) {
542             int min = i * ROTATION_STEP;
543             int max = (i + 1) * ROTATION_STEP;
544
545             if (ret < (min + max) / 2) {
546                 ret = min;
547                 break;
548             }
549         }
550         return ret;
551     }
552 }