1/*
2* Copyright (c) 2018 (https://github.com/phase1geo/Minder)
3*
4* This program is free software; you can redistribute it and/or
5* modify it under the terms of the GNU General Public
6* License as published by the Free Software Foundation; either
7* version 2 of the License, or (at your option) any later version.
8*
9* This program is distributed in the hope that it will be useful,
10* but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*
19* Authored by: Trevor Williams <phase1geo@gmail.com>
20*/
21
22public class ExportYed : Export {
23
24  /* Constructor */
25  public ExportYed() {
26    base( "yed", _( "Yed" ), { ".graphml" }, true, false );
27  }
28
29  /* Exports the given drawing area to the file of the given name */
30  public override bool export( string fname, DrawArea da ) {
31    Xml.Doc*  doc  = new Xml.Doc( "1.0" );
32    Xml.Node* root = new Xml.Node( null, "graphml" );
33    root->new_prop( "xmlns", "http://graphml.graphdrawing.org/xmlns" );
34    root->new_ns( "http://www.yworks.com/xml/yfiles-common/1.0/java", "java" );
35    root->new_ns( "http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0", "sys" );
36    root->new_ns( "http://www.yworks.com/xml/yfiles-common/markup/2.0", "x" );
37    root->new_ns( "http://www.w3.org/2001/XMLSchema-instance", "xsi" );
38    root->new_ns( "http://www.yworks.com/xml/yed/3", "yed" );
39    Xml.Ns* yns = root->new_ns( "http://www.yworks.com/xml/graphml", "y" );
40
41    root->add_child( new Xml.Node.comment( _( "Generated by Minder" ) + " " + Minder.version ) );
42
43    export_keys( root );
44    export_graphs( root, yns, da );
45    doc->set_root_element( root );
46    doc->save_format_file( fname, 1 );
47    delete doc;
48    return( true );
49  }
50
51  /* Returns a single key populated with the specified information */
52  private Xml.Node* export_key_attr( string id, string for_item, string attr_name, string attr_type ) {
53    Xml.Node* n = new Xml.Node( null, "key" );
54    n->new_prop( "id", id );
55    n->new_prop( "for", for_item );
56    n->new_prop( "attr.name", attr_name );
57    n->new_prop( "attr.type", attr_type );
58    return( n );
59  }
60
61  /* Returns a single key populated with the specified information */
62  private Xml.Node* export_key_yfiles( string id, string for_item, string yfiles_type ) {
63    Xml.Node* n = new Xml.Node( null, "key" );
64    n->new_prop( "id", id );
65    n->new_prop( "for", for_item );
66    n->new_prop( "yfiles.type", yfiles_type );
67    return( n );
68  }
69
70  /* Adds all of the keys to the root node */
71  private void export_keys( Xml.Node* root ) {
72    root->add_child( export_key_attr(   "d5", "node", "description", "string" ) );
73    root->add_child( export_key_yfiles( "d6", "node", "nodegraphics" ) );
74    root->add_child( export_key_yfiles( "d7", "graphml", "resources" ) );
75    root->add_child( export_key_attr(   "d9", "edge", "description", "string" ) );
76    root->add_child( export_key_yfiles( "d10", "edge", "edgegraphics" ) );
77  }
78
79  /* Exports each tree as a separate graph */
80  private void export_graphs( Xml.Node* root, Xml.Ns* yns, DrawArea da ) {
81
82    Xml.Node* graph = new Xml.Node( null, "graph" );
83    graph->new_prop( "edgedefault", "directed" );
84    graph->new_prop( "id", "G0" );
85    root->add_child( graph );
86
87    /* Add nodes */
88    for( int i=0; i<da.get_nodes().length; i++ ) {
89      export_node_edge( graph, yns, da.get_nodes().index( i ), da.get_theme() );
90    }
91
92    /* Add connections */
93    export_connections( graph, yns, da.get_connections().connections, da.get_theme() );
94
95    Xml.Node* d7 = new Xml.Node( null, "data" );
96    d7->new_prop( "key", "d7" );
97
98    Xml.Node* res = new Xml.Node( yns, "Resources" );
99    d7->add_child( res );
100
101    graph->add_child( d7 );
102
103  }
104
105  private Xml.Node* export_node_shape( Node node, Theme theme, Xml.Ns* yns ) {
106    Xml.Node* shape = new Xml.Node( yns, "ShapeNode" );
107
108    Xml.Node* geometry = new Xml.Node( yns, "Geometry" );
109    geometry->new_prop( "height", (node.height - (node.style.node_margin * 2)).to_string() );
110    geometry->new_prop( "width",  (node.width  - (node.style.node_margin * 2)).to_string() );
111    geometry->new_prop( "x",      (node.posx + node.style.node_margin).to_string() );
112    geometry->new_prop( "y",      (node.posy + node.style.node_margin).to_string() );
113    shape->add_child( geometry );
114
115    Xml.Node* fill = new Xml.Node( yns, "Fill" );
116    fill->new_prop( "color", Utils.color_from_rgba( node.is_root() ? theme.get_color( "root_background" ) : (node.style.node_fill ? node.link_color : theme.get_color( "background" )) ) );
117    fill->new_prop( "transparent", "false" );
118    shape->add_child( fill );
119
120    Xml.Node* bs = new Xml.Node( yns, "BorderStyle" );
121    bs->new_prop( "color", Utils.color_from_rgba( node.link_color ) );
122    bs->new_prop( "type", "line" );  // TBD
123    bs->new_prop( "width", node.style.node_borderwidth.to_string() );
124    shape->add_child( bs );
125
126    Xml.Node* lbl = new Xml.Node( yns, "NodeLabel" );
127    lbl->new_prop( "alignment", "left" );
128    lbl->new_prop( "autoSizePolicy", "content" );
129    lbl->new_prop( "fontFamily", node.style.node_font.get_family() );
130    lbl->new_prop( "fontSize",   (node.style.node_font.get_size() / Pango.SCALE).to_string() );
131    lbl->new_prop( "fontStyle", "plain" );
132    lbl->new_prop( "hasBackgroundColor", "false" );
133    lbl->new_prop( "hasLineColor", "false" );
134    lbl->new_prop( "height", node.name.height.to_string() );
135    lbl->new_prop( "horizontalTextPosition", "left" );
136    lbl->new_prop( "iconTextGap", "4" );
137    lbl->new_prop( "modelName", "custom" );
138    lbl->new_prop( "textColor", Utils.color_from_rgba( node.is_root() ? theme.get_color( "root_foreground" ) : (node.style.node_fill ? theme.get_color( "background" ) : theme.get_color( "foreground" )) ) );
139    lbl->new_prop( "verticalTextPosition", "top" );
140    lbl->new_prop( "visible", node.folded ? "false" : "true" );
141    lbl->new_prop( "width", node.name.width.to_string() );
142    lbl->new_prop( "x", node.style.node_padding.to_string() );
143    lbl->new_prop( "xml:space", "preserve" );
144    lbl->new_prop( "y", node.style.node_padding.to_string() );
145    lbl->add_content( node.name.get_wrapped_text() );
146    shape->add_child( lbl );
147
148    Xml.Node* model = new Xml.Node( yns, "LabelModel" );
149    Xml.Node* smodel = new Xml.Node( yns, "SmartNodeLabelModel" );
150    smodel->new_prop( "distance", "4.0" );
151    model->add_child( smodel );
152    lbl->add_child( model );
153
154    Xml.Node* param  = new Xml.Node( yns, "ModelParameter" );
155    Xml.Node* sparam = new Xml.Node( yns, "SmartNodeLabelModelParameter" );
156    sparam->new_prop( "labelRatioX", "0.0" );
157    sparam->new_prop( "labelRatioY", "0.0" );
158    sparam->new_prop( "nodeRatioX", "0.0" );
159    sparam->new_prop( "nodeRatioY", "0.0" );
160    sparam->new_prop( "offsetX", "0.0" );
161    sparam->new_prop( "offsetY", "0.0" );
162    sparam->new_prop( "upX", "0.0" );
163    sparam->new_prop( "upY", "-1.0" );
164    param->add_child( sparam );
165    lbl->add_child( param );
166
167    Xml.Node* s = new Xml.Node( yns, "Shape" );
168    switch( node.style.node_border.name() ) {
169      case "rounded" :  s->new_prop( "type", "roundrectangle" );  break;
170      case "squared" :  s->new_prop( "type", "rectangle" );       break;
171      default        :  s->new_prop( "type", "rectangle" );       break;
172    }
173    shape->add_child( s );
174
175    return( shape );
176
177  }
178
179  private Xml.Node* export_node( Node node, Theme theme, Xml.Ns* yns ) {
180    Xml.Node* n  = new Xml.Node( null, "node" );
181    Xml.Node* d5 = new Xml.Node( null, "data" );
182    Xml.Node* d6 = new Xml.Node( null, "data" );
183    n->new_prop( "id", ("n" + node.id().to_string()) );
184    d5->new_prop( "key", "d5" );
185    if( node.note != "" ) {
186      d5->new_prop( "xml:space", "preserve" );
187      d5->add_content( node.note );
188    }
189    n->add_child( d5 );
190    d6->new_prop( "key", "d6" );
191    d6->add_child( export_node_shape( node, theme, yns ) );
192    n->add_child( d6 );
193    return( n );
194  }
195
196  private Xml.Node* export_node_bezieredge( Node node, Xml.Ns* yns ) {
197
198    Xml.Node* be   = new Xml.Node( yns, "BezierEdge" );
199
200    Xml.Node* path = new Xml.Node( yns, "Path" );
201    path->new_prop( "sx", "0.0" );
202    path->new_prop( "sy", "0.0" );
203    path->new_prop( "tx", "0.0" );
204    path->new_prop( "ty", "0.0" );
205    be->add_child( path );
206
207    Xml.Node* ls = new Xml.Node( yns, "LineStyle" );
208    ls->new_prop( "color", Utils.color_from_rgba( node.link_color ) );
209    ls->new_prop( "type", "line" );
210    ls->new_prop( "width", node.style.link_width.to_string() );
211    be->add_child( ls );
212
213    Xml.Node* arrow = new Xml.Node( yns, "Arrows" );
214    arrow->new_prop( "source", "none" );
215    arrow->new_prop( "target", "none" );
216    be->add_child( arrow );
217
218    return( be );
219
220  }
221
222  /* Adds the node link as an edge */
223  private Xml.Node* export_link( Node node, Xml.Ns* yns ) {
224    if( node.is_root() ) return( null );
225    Xml.Node* e = new Xml.Node( null, "edge" );
226    e->new_prop( "id", ("e" + node.id().to_string()) );
227    e->new_prop( "source", ("n" + node.parent.id().to_string()) );
228    e->new_prop( "target", ("n" + node.id().to_string()) );
229
230    Xml.Node* d9 = new Xml.Node( null, "data" );
231    d9->new_prop( "key", "d9" );
232    e->add_child( d9 );
233
234    Xml.Node* d10 = new Xml.Node( null, "data" );
235    d10->new_prop( "key", "d10" );
236    d10->add_child( export_node_bezieredge( node, yns ) );
237    e->add_child( d10 );
238
239    return( e );
240
241  }
242
243  /* Adds a node along with its edge */
244  private void export_node_edge( Xml.Node* graph, Xml.Ns* yns, Node node, Theme theme ) {
245    graph->add_child( export_node( node, theme, yns ) );
246    if( !node.is_root() ) {
247      graph->add_child( export_link( node, yns ) );
248    }
249    for( int i=0; i<node.children().length; i++ ) {
250      export_node_edge( graph, yns, node.children().index( i ), theme );
251    }
252  }
253
254  /* Create connection */
255  private void export_connections( Xml.Node* graph, Xml.Ns* yns, Array<Connection> conns, Theme theme ) {
256
257    for( int i=0; i<conns.length; i++ ) {
258
259      var conn = conns.index( i );
260
261      Xml.Node* e = new Xml.Node( null, "edge" );
262      e->new_prop( "id", ("c" + i.to_string()) );
263      e->new_prop( "source", ("n" + conn.from_node.id().to_string()) );
264      e->new_prop( "target", ("n" + conn.to_node.id().to_string()) );
265
266      Xml.Node* d9 = new Xml.Node( null, "data" );
267      d9->new_prop( "key", "d9" );
268      e->add_child( d9 );
269
270      Xml.Node* d10 = new Xml.Node( null, "data" );
271      d10->new_prop( "key", "d10" );
272
273      Xml.Node* be = new Xml.Node( yns, "BezierEdge" );
274
275      Xml.Node* path = new Xml.Node( yns, "Path" );
276      path->new_prop( "sx", "0.0" );
277      path->new_prop( "sy", "0.0" );
278      path->new_prop( "tx", "0.0" );
279      path->new_prop( "ty", "0.0" );
280      be->add_child( path );
281
282      Xml.Node* ls = new Xml.Node( yns, "LineStyle" );
283      ls->new_prop( "color", Utils.color_from_rgba( theme.get_color( "connection" ) ) );
284      ls->new_prop( "type", (conn.style.connection_dash.name == "solid") ? "line" : "dashed" );
285      ls->new_prop( "width", conn.style.connection_line_width.to_string() );
286      be->add_child( ls );
287
288      Xml.Node* arrow = new Xml.Node( yns, "Arrows" );
289      var       atype = conn.style.connection_arrow;
290      arrow->new_prop( "source", ((atype == "tofrom") || (atype == "both")) ? "standard" : "none" );
291      arrow->new_prop( "target", ((atype == "fromto") || (atype == "both")) ? "standard" : "none" );
292      be->add_child( arrow );
293
294      if( conn.title != null ) {
295
296        Xml.Node* el = new Xml.Node( yns, "EdgeLabel" );
297        el->new_prop( "alignment", "center" );
298        el->new_prop( "fontFamily", conn.style.connection_font.get_family() );
299        el->new_prop( "fontSize", (conn.style.connection_font.get_size() / Pango.SCALE).to_string() );
300        el->new_prop( "hasBackgroundColor", "false" );
301        el->new_prop( "hasLineColor", "true" );
302        el->new_prop( "height", conn.title.height.to_string() );
303        el->new_prop( "horizontalTextPosition", "center" );
304        el->new_prop( "modelName", "custom" );
305        el->new_prop( "preferredPlacement", "anywhere" );
306        el->new_prop( "ratio", "0.5" );
307        el->new_prop( "textColor", Utils.color_from_rgba( theme.get_color( "foreground" ) ) );
308        el->new_prop( "verticalTextPosition", "top" );
309        el->new_prop( "visible", "true" );
310        el->new_prop( "width", conn.title.width.to_string() );
311        el->new_prop( "x", conn.title.posx.to_string() );
312        el->new_prop( "xml:space", "preserve" );
313        el->new_prop( "y", conn.title.posy.to_string() );
314        el->add_content( conn.title.get_wrapped_text() );
315
316        Xml.Node* lm  = new Xml.Node( yns, "LabelModel" );
317        Xml.Node* slm = new Xml.Node( yns, "SmartEdgeLabelModel" );
318        slm->new_prop( "autoRotationEnabled", "false" );
319        slm->new_prop( "defaultAngle", "0.0" );
320        slm->new_prop( "defaultDistance", "10.0" );
321        lm->add_child( slm );
322        el->add_child( lm );
323
324        Xml.Node* mp  = new Xml.Node( yns, "ModelParameter" );
325        Xml.Node* smp = new Xml.Node( yns, "SmartEdgeLabelModelParameter" );
326        smp->new_prop( "angle", "0.0" );
327        smp->new_prop( "distance", "30.0" );
328        smp->new_prop( "distanceToCenter", "true" );
329        smp->new_prop( "position", "left" );
330        smp->new_prop( "ratio", "0.5" );
331        smp->new_prop( "segment", "1" );
332        mp->add_child( smp );
333        el->add_child( mp );
334
335        Xml.Node* ppd = new Xml.Node( yns, "PreferredPlacementDescriptor" );
336        ppd->new_prop( "angle", "0.0" );
337        ppd->new_prop( "angleOffsetOnRightSide", "0" );
338        ppd->new_prop( "angleReference", "absolute" );
339        ppd->new_prop( "angleRotationOnRightSide", "co" );
340        ppd->new_prop( "distance", "-1.0" );
341        ppd->new_prop( "frozen", "true" );
342        ppd->new_prop( "placement", "anywhere" );
343        ppd->new_prop( "side", "anywhere" );
344        ppd->new_prop( "sideReference", "relative_to_edge_flow" );
345        el->add_child( ppd );
346
347        be->add_child( el );
348      }
349
350      d10->add_child( be );
351      e->add_child( d10 );
352      graph->add_child( e );
353
354    }
355
356  }
357
358}
359