Xcos: fix blocks auto-resize
[scilab.git] / scilab / modules / xcos / src / java / org / scilab / modules / xcos / graph / model / XcosCellFactory.java
1 /*
2  * Scilab ( http://www.scilab.org/ ) - This file is part of Scilab
3  * Copyright (C) 2015-2015 - Scilab Enterprises - Clement DAVID
4  *
5  * Copyright (C) 2012 - 2016 - Scilab Enterprises
6  *
7  * This file is hereby licensed under the terms of the GNU GPL v2.0,
8  * pursuant to article 5.3.4 of the CeCILL v.2.1.
9  * This file was originally licensed under the terms of the CeCILL v2.1,
10  * and continues to be available under such terms.
11  * For more information, see the COPYING file which you should have received
12  * along with this program.
13  *
14  */
15
16 package org.scilab.modules.xcos.graph.model;
17
18 import static org.scilab.modules.action_binding.highlevel.ScilabInterpreterManagement.buildCall;
19 import static org.scilab.modules.action_binding.highlevel.ScilabInterpreterManagement.synchronousScilabExec;
20
21 import java.lang.reflect.InvocationTargetException;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.EnumSet;
25 import java.util.HashMap;
26 import java.util.Optional;
27 import java.util.logging.Logger;
28
29 import org.scilab.modules.action_binding.highlevel.ScilabInterpreterManagement.InterpreterException;
30 import org.scilab.modules.graph.utils.ScilabExported;
31 import org.scilab.modules.xcos.JavaController;
32 import org.scilab.modules.xcos.Kind;
33 import org.scilab.modules.xcos.ObjectProperties;
34 import org.scilab.modules.xcos.VectorOfDouble;
35 import org.scilab.modules.xcos.VectorOfScicosID;
36 import org.scilab.modules.xcos.Xcos;
37 import org.scilab.modules.xcos.XcosView;
38 import org.scilab.modules.xcos.block.BasicBlock;
39 import org.scilab.modules.xcos.graph.XcosDiagram;
40 import org.scilab.modules.xcos.link.BasicLink;
41 import org.scilab.modules.xcos.link.CommandControlLink;
42 import org.scilab.modules.xcos.link.ExplicitLink;
43 import org.scilab.modules.xcos.link.ImplicitLink;
44 import org.scilab.modules.xcos.port.BasicPort;
45 import org.scilab.modules.xcos.port.command.CommandPort;
46 import org.scilab.modules.xcos.port.control.ControlPort;
47 import org.scilab.modules.xcos.port.input.ExplicitInputPort;
48 import org.scilab.modules.xcos.port.input.ImplicitInputPort;
49 import org.scilab.modules.xcos.port.output.ExplicitOutputPort;
50 import org.scilab.modules.xcos.port.output.ImplicitOutputPort;
51 import org.scilab.modules.xcos.utils.BlockPositioning;
52
53 import com.mxgraph.model.mxCell;
54 import com.mxgraph.model.mxGeometry;
55 import com.mxgraph.util.mxPoint;
56 import java.lang.reflect.Constructor;
57 import java.util.EnumMap;
58 import org.scilab.modules.xcos.block.SplitBlock;
59 import org.scilab.modules.xcos.block.positionning.RoundBlock;
60 ;
61
62 /**
63  * Ease the creation of any {@link Kind} of graphical object
64  */
65 public final class XcosCellFactory {
66
67     /** Size compatibility for user defined blocks */
68     private static final double DEFAULT_SIZE_FACTOR = 20.0;
69     private static final Logger LOG = Logger.getLogger(XcosCellFactory.class.getName());
70
71     /** Default singleton constructor */
72     private XcosCellFactory() {
73         // This class is a static singleton
74     }
75
76     /**
77      * This is a notify method mapped as a Scilab gateway used to alert with the loaded UID
78      *
79      * @param uid
80      *            the loaded UID
81      * @param kind
82      *            the kind of the created object (as an int)
83      */
84     @ScilabExported(module = "xcos", filename = "XcosCellFactory.giws.xml")
85     public static synchronized void created(long uid, int kind) {
86         lastCreated = new ScicosObjectOwner(uid, Kind.values()[kind]);
87
88     }
89
90     private static ScicosObjectOwner lastCreated = null;
91
92     /**
93      * Retrieve and clear the last created object (<pre>xcosCellCreated</pre> call)
94      * @return the last created object
95      */
96     public static synchronized ScicosObjectOwner getLastCreated() {
97         ScicosObjectOwner last = lastCreated;
98         lastCreated = null;
99         return last;
100     }
101
102     /*
103      * Diagram management
104      */
105
106     /**
107      * Allocate a Java XcosDiagram from a COSF file.
108      *
109      * This method execute the file and register a
110      *
111      * @param controller
112      *            the controller
113      * @param filename
114      *            the file to execute
115      * @return an allocated XcosDiagram
116      */
117     public static XcosDiagram createDiagramFromCOSF(final JavaController controller, String filename) {
118         XcosView view = (XcosView) JavaController.lookup_view(Xcos.class.getName());
119         JavaController.unregister_view(view);
120
121         XcosDiagram diagram;
122         try {
123             synchronousScilabExec(
124                 "function f(), " + buildCall("exec", filename, -1) + "; " + buildCall("xcosCellCreated", "scs_m".toCharArray()) + "endfunction; f();");
125
126             ScicosObjectOwner last = getLastCreated();
127             if (last.getKind() == Kind.DIAGRAM) {
128                 String[] strUID = new String[1];
129                 controller.getObjectProperty(last.getUID(), last.getKind(), ObjectProperties.UID, strUID);
130
131                 diagram = new XcosDiagram(controller, last.getUID(), last.getKind(), strUID[0]);
132                 insertChildren(controller, diagram);
133             } else {
134                 diagram = null;
135             }
136         } catch (InterpreterException e) {
137             diagram = null;
138         } finally {
139             JavaController.register_view(Xcos.class.getName(), view);
140         }
141
142         return diagram;
143     }
144
145     /**
146      * Insert the diagram MVC children into the JGraphX model
147      *
148      * @param controller
149      *            the shared controller
150      * @param diagram
151      *            the current diagram instance
152      */
153     public static void insertChildren(JavaController controller, XcosDiagram diagram) {
154         /*
155          * Retrieve the children
156          */
157         VectorOfScicosID children = new VectorOfScicosID();
158         controller.getObjectProperty(diagram.getUID(), diagram.getKind(), ObjectProperties.CHILDREN, children);
159         final int childrenLen = children.size();
160
161         /*
162          * Allocation some pre-sized stash data
163          */
164         final ArrayList<BasicLink> links = new ArrayList<>(childrenLen / 2);
165         final HashMap<Long, BasicPort> ports = new HashMap<>(childrenLen);
166
167         /*
168          * Create the XcosCell objects and store some of them for later use
169          */
170         XcosCell[] cells = new XcosCell[childrenLen];
171         for (int i = 0; i < childrenLen; i++) {
172             final long uid = children.get(i);
173             final Kind kind = controller.getKind(uid);
174
175             switch (kind) {
176                 case ANNOTATION:
177                 case BLOCK:
178                     BasicBlock b = createBlock(controller, uid, kind);
179                     cells[i] = b;
180                     BlockPositioning.updatePortsPosition(diagram, b);
181                     b.getTypedChildrenIndexes(BasicPort.class).stream()
182                     .map(index -> b.getChildAt(index))
183                     .filter(c -> c instanceof BasicPort)
184                     .map(c -> (BasicPort) c)
185                     .forEach(c -> ports.put(c.getUID(), c));
186                     break;
187                 case LINK:
188                     BasicLink l = createLink(controller, uid, kind);
189                     cells[i] = l;
190                     links.add(l);
191                     break;
192                 default:
193                     break;
194             }
195         }
196
197         /*
198          * Relink the links on the XcosCell part
199          */
200         for (BasicLink l : links) {
201             long[] src = new long[1];
202             controller.getObjectProperty(l.getUID(), l.getKind(), ObjectProperties.SOURCE_PORT, src);
203
204             long[] dest = new long[1];
205             controller.getObjectProperty(l.getUID(), l.getKind(), ObjectProperties.DESTINATION_PORT, dest);
206
207             BasicPort srcPort = ports.get(src[0]);
208             if (srcPort != null) {
209                 l.setSource(srcPort);
210             } else {
211                 LOG.severe("Unable to connect link " + l.getId() + " : invalid source " + src[0]);
212             }
213
214             BasicPort destPort = ports.get(dest[0]);
215             if (destPort != null) {
216                 l.setTarget(destPort);
217             } else {
218                 LOG.severe("Unable to connect link " + l.getId() + " : invalid target " + dest[0]);
219             }
220         }
221
222         // re-add the children cells without duplicating them
223         children.clear();
224         controller.setObjectProperty(diagram.getUID(), diagram.getKind(), ObjectProperties.CHILDREN, children);
225
226         // add all the children using the diagram modification tracking features
227         diagram.addCells(cells);
228
229         // each cell has been referenced twice (CHILDREN insert and addCells), derefence them all by one
230         Arrays.stream(cells).forEach(c -> controller.deleteObject(c.getUID()));
231     }
232
233     /*
234      * Block and Annotation management
235      */
236
237     /**
238      * Instantiate a new block with the specified interface function.
239      *
240      * @param func
241      *            the interface function
242      * @return A new instance of a block.
243      */
244     public static BasicBlock createBlock(BlockInterFunction func) throws InterpreterException {
245         return createBlock(func, func.name());
246     }
247
248     /**
249      * Instantiate a new block with the specified UID value and interface function
250      *
251      * @param uid
252      *            The associated UID value
253      * @param interfaceFunction
254      *            the interface function
255      * @return A new instance of a block.
256      */
257     public static BasicBlock createBlock(String interfaceFunction) throws InterpreterException {
258         Optional<BlockInterFunction> func = EnumSet.allOf(BlockInterFunction.class).stream().filter(f -> f.name().equals(interfaceFunction)).findFirst();
259
260         final BasicBlock block;
261         if (func.isPresent()) {
262             block = createBlock(func.get());
263         } else {
264             block = createBlock(BlockInterFunction.BASIC_BLOCK, interfaceFunction);
265         }
266         block.setStyle(interfaceFunction);
267
268         return block;
269     }
270
271     private static BasicBlock createBlock(BlockInterFunction func, String interfaceFunction) throws InterpreterException {
272         return createBlock(new JavaController(), func, interfaceFunction);
273     }
274
275     private static BasicBlock createBlock(final JavaController controller, BlockInterFunction func, String interfaceFunction) throws InterpreterException {
276         BasicBlock block;
277         ScicosObjectOwner last;
278
279         if (BlockInterFunction.BASIC_BLOCK.name().equals(interfaceFunction)) {
280             // deliver all the MVC speed for the casual case
281             last = new ScicosObjectOwner(controller.createObject(Kind.BLOCK), Kind.BLOCK);
282         } else {
283             // allocate an empty block that will be filled later
284             synchronousScilabExec("xcosCellCreated(" + interfaceFunction + "(\"define\")); ");
285             last = getLastCreated();
286         }
287
288         // defensive programming
289         if (last == null) {
290             throw new InterpreterException("XcosCellFactory#createBlock : unable to allocate " + interfaceFunction);
291         }
292
293         if (EnumSet.of(Kind.BLOCK, Kind.ANNOTATION).contains(last.getKind())) {
294             block = createBlock(controller, func, interfaceFunction, last.getUID(), last.getKind());
295         } else {
296             block = null;
297         }
298
299         return block;
300     }
301
302     private static BasicBlock createBlock(final JavaController controller, long uid, Kind kind) {
303         String[] interfaceFunction = new String[1];
304         if (kind == Kind.BLOCK) {
305             controller.getObjectProperty(uid, kind, ObjectProperties.INTERFACE_FUNCTION, interfaceFunction);
306         } else { // ANNOTATION
307             interfaceFunction[0] = "TEXT_f";
308         }
309
310         final BlockInterFunction func = lookForInterfunction(interfaceFunction[0]);
311
312         return createBlock(controller, func, interfaceFunction[0], uid, kind);
313     }
314
315     public static BlockInterFunction lookForInterfunction(String interfaceFunction) {
316         Optional<BlockInterFunction> optFunc = EnumSet.allOf(BlockInterFunction.class).stream().filter(f -> f.name().equals(interfaceFunction)).findFirst();
317
318         final BlockInterFunction func;
319         if (optFunc.isPresent()) {
320             func = optFunc.get();
321         } else {
322             func = BlockInterFunction.BASIC_BLOCK;
323         }
324         return func;
325     }
326
327     /**
328      * Instantiate a new block with the specified interface function and uid.
329      *
330      * @param controller
331      *            the Java controller to use
332      * @param func
333      *            the interface function as an enum
334      * @param interfaceFunction
335      *            the interface function name
336      * @param uid
337      *            the allocated uid
338      * @return A new instance of a block.
339      */
340     public static BasicBlock createBlock(final JavaController controller, BlockInterFunction func, String interfaceFunction, long uid, Kind kind) {
341
342         final EnumMap<ObjectProperties, Integer> properties = new EnumMap<>(ObjectProperties.class);
343         properties.put(ObjectProperties.INPUTS, 0);
344         properties.put(ObjectProperties.OUTPUTS, 0);
345         properties.put(ObjectProperties.EVENT_INPUTS, 0);
346         properties.put(ObjectProperties.EVENT_OUTPUTS, 0);
347
348
349         /*
350          * Retrieve the JGraphX data before cell creation
351          */
352         String[] strUID = new String[1];
353         controller.getObjectProperty(uid, kind, ObjectProperties.UID, strUID);
354
355         String[] style = new String[1];
356         controller.getObjectProperty(uid, kind, ObjectProperties.STYLE, style);
357         if (style[0].isEmpty()) {
358             style[0] = interfaceFunction;
359         }
360
361         String value;
362         String[] description = new String[] { "" };
363         controller.getObjectProperty(uid, kind, ObjectProperties.DESCRIPTION, description);
364         value = description[0];
365
366         VectorOfDouble geom = new VectorOfDouble(4);
367         controller.getObjectProperty(uid, kind, ObjectProperties.GEOMETRY, geom);
368
369         double x = geom.get(0);
370         double y = geom.get(1);
371         double w = geom.get(2);
372         double h = geom.get(3);
373         mxGeometry geometry = new mxGeometry(x, y, w, h);
374
375         /*
376          * Instanciate the block
377          */
378         BasicBlock block = null;
379         try {
380             Constructor<? extends BasicBlock> cstr = func.getKlass().getConstructor(JavaController.class, Long.TYPE, Kind.class, Object.class, mxGeometry.class, String.class, String.class);
381             block = cstr.newInstance(controller, uid, kind, value, geometry, style[0], strUID[0]);
382         } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
383                      | SecurityException e) {
384             // Something goes wrong, print it.
385             e.printStackTrace();
386             return block;
387         }
388
389         /*
390          * Synchronize model information back to the JGraphX data
391          *
392          * Annotations have no inputs/outputs
393          */
394
395         if (block.getKind() == Kind.BLOCK) {
396             insertPortChildren(controller, properties, block);
397         }
398
399
400         /*
401          * Compatibility to ease user definition :
402          *   * split are not updated as they have an hard-coded fixed-size
403          *   * round blocks : ports globally layout around the block
404          *   * generic case : layout the ports per kind per block-side
405          */
406         boolean convertGeometry;
407         if (block instanceof SplitBlock) {
408             convertGeometry = false;
409         } else if (block instanceof RoundBlock) {
410             int numberOfPorts = properties.get(ObjectProperties.INPUTS) +
411                                 properties.get(ObjectProperties.OUTPUTS) +
412                                 properties.get(ObjectProperties.EVENT_INPUTS) +
413                                 properties.get(ObjectProperties.EVENT_OUTPUTS);
414             convertGeometry = (2 * w + 2 * h) < (numberOfPorts * BasicPort.DEFAULT_PORTSIZE);
415         } else {
416             convertGeometry = h < (properties.get(ObjectProperties.INPUTS) * BasicPort.DEFAULT_PORTSIZE) |
417                               h < (properties.get(ObjectProperties.OUTPUTS) * BasicPort.DEFAULT_PORTSIZE) |
418                               w < (properties.get(ObjectProperties.EVENT_INPUTS) * BasicPort.DEFAULT_PORTSIZE) |
419                               w < (properties.get(ObjectProperties.EVENT_OUTPUTS) * BasicPort.DEFAULT_PORTSIZE);
420         }
421
422         if (convertGeometry) {
423             w = w * DEFAULT_SIZE_FACTOR;
424             h = h * DEFAULT_SIZE_FACTOR;
425
426             /*
427              * Invert the y-axis value and translate it.
428              */
429             y = -y - h;
430
431             block.setGeometry(new mxGeometry(x, y, w, h));
432         }
433
434         return block;
435     }
436
437     /**
438      * Instantiate a new block for an already created MVC object
439      *
440      * @param lastCreated the owned MVC object
441      * @return a block or null
442      */
443     public static BasicBlock createBlock(final JavaController controller, ScicosObjectOwner lastCreated) {
444         // pre-condition
445         if (lastCreated.getKind() != Kind.ANNOTATION && lastCreated.getKind() != Kind.BLOCK) {
446             return null;
447         }
448
449         String[] interfaceFunction = new String[1];
450         BlockInterFunction func = lookForInterfunction(interfaceFunction[0]);
451
452         return createBlock(controller, func, interfaceFunction[0], lastCreated.getUID(), lastCreated.getKind());
453     }
454
455     /*
456      * Port management
457      */
458
459     /**
460      * Helper used to create port children on a parent block.
461      *
462      * This method does not manage the model transaction and should be used to preset the children of a block out of an {@link XcosDiagram}.
463      *
464      * @param controller
465      *            is the shared controller instance
466      * @param properties
467      *            specify the kind of port to insert and should be some of :
468      *            <UL>
469      *            <LI>{@link ObjectProperties#INPUTS}
470      *            <LI>{@link ObjectProperties#OUTPUTS}
471      *            <LI>{@link ObjectProperties#EVENT_INPUTS}
472      *            <LI>{@link ObjectProperties#EVENT_OUTPUTS}
473      *            </UL>
474      *            This method will fill the value with the number of added ports
475      * @param parent
476      *            is the parent {@link mxCell} to modify
477      */
478     private static void insertPortChildren(final JavaController controller, final EnumMap<ObjectProperties, Integer> properties, final XcosCell parent) {
479         for (ObjectProperties property : properties.keySet()) {
480             properties.put(property, insertPortChildren(controller, property, parent));
481         }
482     }
483
484     /**
485      * Helper used to create port children on a parent block.
486      *
487      * This method does not manage the model transaction and should be used to preset the children of a block out of an {@link XcosDiagram}.
488      *
489      * @param controller
490      *            is the shared controller instance
491      * @param property
492      *            specify the kind of port to insert and should be one of :
493      *            <UL>
494      *            <LI>{@link ObjectProperties#INPUTS}
495      *            <LI>{@link ObjectProperties#OUTPUTS}
496      *            <LI>{@link ObjectProperties#EVENT_INPUTS}
497      *            <LI>{@link ObjectProperties#EVENT_OUTPUTS}
498      * @param parent
499      *            is the parent {@link mxCell} to modify
500      * @return the number of inserted children
501      */
502     private static int insertPortChildren(final JavaController controller, final ObjectProperties property, final XcosCell parent) {
503         VectorOfScicosID modelChildren = new VectorOfScicosID();
504         controller.getObjectProperty(parent.getUID(), parent.getKind(), property, modelChildren);
505
506         XcosCell[] children = new XcosCell[modelChildren.size()];
507         for (int i = 0; i < children.length; i++) {
508             XcosCell child = createPort(controller, modelChildren.get(i), property);
509             children[i] = child;
510         }
511
512         modelChildren.clear();
513         controller.setObjectProperty(parent.getUID(), parent.getKind(), property, modelChildren);
514
515         Arrays.stream(children).forEach(c -> {
516             parent.insert(c);
517             controller.deleteObject(c.getUID());
518         });
519
520         return children.length;
521     }
522
523     /**
524      * Create a port for a specific uid
525      *
526      * @param controller
527      *            is the shared controller instance
528      * @param uid
529      *            represent the allocated UID on the MVC
530      * @param property
531      *            specify the kind of port to create and should be one of :
532      *            <UL>
533      *            <LI>{@link ObjectProperties#INPUTS}
534      *            <LI>{@link ObjectProperties#OUTPUTS}
535      *            <LI>{@link ObjectProperties#EVENT_INPUTS}
536      *            <LI>{@link ObjectProperties#EVENT_OUTPUTS}
537      * @return a newly allocated port
538      */
539     private static final BasicPort createPort(final JavaController controller, long uid, final ObjectProperties property) {
540         BasicPort port;
541         boolean[] isImplicit = { false };
542
543         String[] strUID = new String[] { "" };
544         controller.getObjectProperty(uid, Kind.PORT, ObjectProperties.UID, strUID);
545
546         String[] style = new String[] { "" };
547         controller.getObjectProperty(uid, Kind.PORT, ObjectProperties.STYLE, style);
548
549         String[] value = new String[] { "" };
550         controller.getObjectProperty(uid, Kind.PORT, ObjectProperties.LABEL, value);
551
552         switch (property) {
553             case INPUTS:
554                 controller.getObjectProperty(uid, Kind.PORT, ObjectProperties.IMPLICIT, isImplicit);
555                 if (isImplicit[0]) {
556                     port = new ImplicitInputPort(controller, uid, Kind.PORT, value[0], style[0], strUID[0]);
557                 } else {
558                     port = new ExplicitInputPort(controller, uid, Kind.PORT, value[0], style[0], strUID[0]);
559                 }
560                 break;
561             case OUTPUTS:
562                 controller.getObjectProperty(uid, Kind.PORT, ObjectProperties.IMPLICIT, isImplicit);
563                 if (isImplicit[0]) {
564                     port = new ImplicitOutputPort(controller, uid, Kind.PORT, value[0], style[0], strUID[0]);
565                 } else {
566                     port = new ExplicitOutputPort(controller, uid, Kind.PORT, value[0], style[0], strUID[0]);
567                 }
568                 break;
569             case EVENT_INPUTS:
570                 port = new ControlPort(controller, uid, Kind.PORT, value[0], style[0], strUID[0]);
571                 break;
572             case EVENT_OUTPUTS:
573                 port = new CommandPort(controller, uid, Kind.PORT, value[0], style[0], strUID[0]);
574                 break;
575             default:
576                 return null;
577         }
578
579         /*
580          * Setup JGraphX properties
581          */
582
583         return port;
584     }
585
586     /*
587      * Link management
588      */
589
590     private static BasicLink createLink(JavaController controller, long uid, Kind kind) {
591         int[] type = new int[1];
592         controller.getObjectProperty(uid, kind, ObjectProperties.KIND, type);
593
594         /*
595          * Synchronize model information back to the JGraphX data
596          */
597
598         String[] value = new String[] { "" };
599         controller.getObjectProperty(uid, kind, ObjectProperties.LABEL, value);
600
601         String[] style = new String[] { "" };
602         controller.getObjectProperty(uid, kind, ObjectProperties.STYLE, style);
603
604         String[] strUID = new String[] { "" };
605         controller.getObjectProperty(uid, kind, ObjectProperties.UID, strUID);
606
607         VectorOfDouble controlPoints = new VectorOfDouble();
608         controller.getObjectProperty(uid, kind, ObjectProperties.CONTROL_POINTS, controlPoints);
609         final int pointsLen = controlPoints.size() / 2;
610
611         mxGeometry geom = new mxGeometry();
612
613         // as the link is supposed to be connected and accordingly to the JGraphX rules : do not add the origin and destination point
614         ArrayList<mxPoint> points = new ArrayList<>();
615         int i = 0;
616         // ignore origin
617         i++;
618         // loop for points
619         for (; i < pointsLen - 1; i++) {
620             points.add(new mxPoint(controlPoints.get(2 * i), controlPoints.get(2 * i + 1)));
621         }
622         // ignore destination
623         i++;
624
625         geom.setPoints(points);
626
627         /*
628          * Allocate the link
629          */
630         BasicLink link;
631         switch (type[0]) {
632             case -1:
633                 link = new CommandControlLink(controller, uid, kind, value[0], geom, style[0], strUID[0]);
634                 break;
635             case 1:
636                 link = new ExplicitLink(controller, uid, kind, value[0], geom, style[0], strUID[0]);
637                 break;
638             case 2:
639                 link = new ImplicitLink(controller, uid, kind, value[0], geom, style[0], strUID[0]);
640                 break;
641             default:
642                 return null;
643         }
644
645         return link;
646     }
647 }