1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.layout.genealogy;
15  
16  import java.awt.BasicStroke;
17  import java.awt.Color;
18  import java.awt.EventQueue;
19  import java.awt.Graphics2D;
20  import java.awt.Stroke;
21  import java.awt.event.ActionEvent;
22  import java.awt.geom.GeneralPath;
23  import java.awt.geom.PathIterator;
24  import java.io.File;
25  import java.io.FilenameFilter;
26  import java.io.IOException;
27  import java.net.URL;
28  import java.util.Locale;
29  import java.util.Map;
30  
31  import javax.swing.AbstractAction;
32  import javax.swing.Action;
33  import javax.swing.JFileChooser;
34  import javax.swing.JMenu;
35  import javax.swing.JMenuBar;
36  import javax.swing.JToolBar;
37  import javax.swing.filechooser.FileFilter;
38  
39  import demo.layout.genealogy.iohandler.GedcomHandler;
40  import demo.view.DemoBase;
41  import demo.view.DemoDefaults;
42  
43  import y.base.Node;
44  import y.base.NodeCursor;
45  import y.base.NodeList;
46  import y.base.NodeMap;
47  import y.io.GraphMLIOHandler;
48  import y.io.graphml.KeyType;
49  import y.layout.genealogy.FamilyTreeLayouter;
50  import y.layout.LayoutTool;
51  import y.layout.BufferedLayouter;
52  import y.module.FamilyTreeLayoutModule;
53  import y.util.D;
54  import y.util.GraphHider;
55  import y.view.BendList;
56  import y.view.BevelNodePainter;
57  import y.view.BridgeCalculator;
58  import y.view.DefaultGraph2DRenderer;
59  import y.view.EdgeRealizer;
60  import y.view.GenericEdgePainter;
61  import y.view.GenericEdgeRealizer;
62  import y.view.GenericNodeRealizer;
63  import y.view.Graph2D;
64  import y.view.LineType;
65  import y.view.NavigationMode;
66  import y.view.NodeLabel;
67  import y.view.NodeRealizer;
68  import y.view.ShinyPlateNodePainter;
69  import y.geom.YPoint;
70  import y.view.SmartNodeLabelModel;
71  
72  
73  /**
74   * This Demo shows how to use the FamilyTreeLayouter.
75   * <p>
76   * <b>Usage:</b>
77   * <br>
78   * Load a Gedcom file with "Load..." from the "File" menu. The gedcom file is converted on the fly into GraphML
79   * and loaded into the graph by the {@link demo.layout.genealogy.iohandler.GedcomHandler}. After loading,
80   * the graph will be laid out by the {@link y.layout.genealogy.FamilyTreeLayouter}.
81   * <br>
82   * NOTE: You will find some sample files in your &lt;src&gt;/demo/view/layout/genealogy/samples folder.
83   * <br>
84   * To re-layout the graph press the "layout" button. An options dialog will open where you can modify
85   * some basic and advanced options. Clicking "OK" will induce a new layout with the new settings.
86   * <br>
87   * To load some sample graphs which are provided with this demo select one from the "Examples" ComboBox.
88   * There are four different family trees with different complexity provided.
89   * <br>
90   * NOTE: For this feature, Gedcom files (ending: .ged) must be exported as resources.
91   * <br>
92   * Clicking on a node will collapse the graph to two generations around the clicked node. The "Show all" button
93   * will expand the graph again
94   * </p>
95   * <p>
96   * API usage:
97   * <br>
98   * The FamilyTreeLayouter needs to distinguish between family nodes, i.e. nodes representing a FAM entry
99   * in the Gedcom file, and nodes representing individuals (i.e. persons, INDI entries). To do so,
100  * a data provider with the key {@link y.layout.genealogy.FamilyTreeLayouter#DP_KEY_FAMILY_TYPE} has to be registered
101  * to the graph. This data provider will return a String which denotes nodes representing individuals.
102  * In this demo, this is achieved by comparing the node's background color with the color, family nodes are
103  * painted with ({@link Color#BLACK}).
104  * <br>
105  * For writing, the GedcomHandler needs to distinguish between family nodes and individuals as well
106  * as between male and female individuals. To do so, a data provider with the key
107  * {@link y.layout.genealogy.FamilyTreeLayouter#DP_KEY_FAMILY_TYPE} has to be registered to the graph.
108  * These data provider will return a String which can be used to distinguish between families, male and female
109  * individual.
110  * In this demo, this is achieved by comparing the node's background color with predefined values.
111  * As this demo is a viewer without editing capabilities, the export function is not implemented in the GUI
112  * (Although it is implemented in the code: see class ExportAction)
113  * <br>
114  * <br>
115  * Note that the GraphML format offers the possibility to extract additional information from the original file.
116  * In this demo, String attributes whether the node represents a family or a male or female individual are mapped
117  * to the graph and can be used as data providers for the layouter and the GedcomHandler.
118  * <br>
119  * To use this feature, modify the code as indicated in the source code.
120  * The methods to modify are {@link FamilyTreeDemo#loadGedcom(String)} and
121  * {@link GedcomHandler#read(y.view.Graph2D, java.io.InputStream)}.
122  * </p>
123  *
124  * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/domain_specific_layouter.html#familytreee_layouter">Section Family Tree Layout</a> in the yFiles for Java Developer's Guide
125  */
126 public class FamilyTreeDemo extends DemoBase {
127   private static final int PREFERRED_FONT_SIZE = 18;
128 
129   private FamilyTreeLayoutModule ftlm;
130   private GraphHider graphHider;
131 
132   /**
133    * Launches this demo.
134    */
135   public static void main(String[] args) {
136     EventQueue.invokeLater(new Runnable() {
137       public void run() {
138         Locale.setDefault(Locale.ENGLISH);
139         initLnF();
140         new FamilyTreeDemo().start();
141       }
142     });
143   }
144   
145   /**
146    * Add a NavigationMode
147    */
148   protected void registerViewModes() {
149     view.addViewMode(new NavigationMode() {
150       public void mouseClicked(double x, double y) {
151         super.mouseClicked(x, y);
152         if(getHitInfo(x,y).getHitNode() != null) {
153           setToMain(getLastHitInfo().getHitNode());
154         }
155       }
156     });
157   }
158   
159   /**
160    * Hide nodes that are not related with the given node, i.e.,
161    * show only the clicked node, its siblings, parents, parents of parents (recursively) as well as its families, children
162    * and children of children (recursively).
163    *
164    * @param clickedNode The node to be the new main node of the graph
165    */
166   private void setToMain(final Node clickedNode) {
167     final Graph2D graph = view.getGraph2D();
168     final YPoint oldCenter = graph.getCenter(clickedNode);
169 
170     graphHider.unhideAll();  //undo the previous selection
171 
172     //mark nodes that should be shown
173     final NodeMap showNodeMap = graph.createNodeMap();
174     Node familyNode = (clickedNode.inDegree() > 0) ? clickedNode.firstInEdge().source() : null;
175     if(familyNode != null) {      
176       for (NodeCursor nc = familyNode.successors(); nc.ok(); nc.next()) {
177         showNodeMap.setBool(nc.node(), true); //mark clicked node and its siblings
178       }
179       NodeList queue = new NodeList(familyNode);
180       while (!queue.isEmpty()) { //mark all predecessors
181         final Node n = queue.popNode();
182         if (!showNodeMap.getBool(n)) { //prevents that a node is handled twice -> should not happen in most families ;-)
183           showNodeMap.setBool(n, true);
184           queue.addAll(n.predecessors());
185         }
186       }
187     } else{
188       showNodeMap.setBool(clickedNode, true);
189     }
190 
191     //also add the successors
192     NodeList queue = new NodeList(clickedNode.successors()); 
193     while (!queue.isEmpty()) {
194       final Node n = queue.popNode();
195       if (!showNodeMap.getBool(n)) {
196         showNodeMap.setBool(n, true);
197         queue.addAll(n.successors());
198 
199         //also show n's direct predecessors, i.e., both parents of a family node
200         for (NodeCursor nc = n.predecessors(); nc.ok(); nc.next()) {
201           final Node pred = nc.node();
202           if (!showNodeMap.getBool(pred)) {
203             showNodeMap.setBool(pred, true);
204           }
205         }
206       }
207     }
208 
209     //hide non marked nodes
210     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
211       final Node n = nc.node();
212       if(!showNodeMap.getBool(n)) {
213         graphHider.hide(n);
214       }
215     }
216     graph.disposeNodeMap(showNodeMap);
217 
218     //apply the layout to the subgraph
219     final FamilyTreeLayouter layouter = new FamilyTreeLayouter();
220     getLayoutModule().configure(layouter);
221     new BufferedLayouter(layouter).doLayout(graph);
222 
223     //move clicked node to its old position
224     final YPoint newCenter = graph.getCenter(clickedNode);
225     LayoutTool.moveSubgraph(graph, graph.nodes(), oldCenter.getX() - newCenter.getX(),
226         oldCenter.getY() - newCenter.getY());
227     view.updateView();
228   }
229 
230   /**
231    * Creates a toolbar for this demo.
232    */
233   protected JToolBar createToolBar() {
234     final Action layoutAction = new AbstractAction(
235             "Layout", SHARED_LAYOUT_ICON) {
236       public void actionPerformed(ActionEvent e) {
237         OptionSupport.showDialog(getLayoutModule(), view.getGraph2D(), true, view.getFrame());
238       }
239     };
240 
241     final Action showAllAction = new AbstractAction("Show all") {
242       public void actionPerformed(ActionEvent e) {
243         if (graphHider != null) {
244           graphHider.unhideAll();
245         }
246         getLayoutModule().start(view.getGraph2D());
247       }
248     };
249 
250     JToolBar jToolBar = super.createToolBar();
251     jToolBar.addSeparator();
252     jToolBar.add(showAllAction);
253     //jToolBar.add(new ExportAction());
254     jToolBar.addSeparator();
255     jToolBar.add(createActionControl(layoutAction));
256     return jToolBar;
257   }
258 
259   protected JMenuBar createMenuBar() {
260     final JMenuBar menuBar = super.createMenuBar();
261     createExamplesMenu(menuBar);
262     return menuBar;
263   }
264 
265   /**
266    * Creates a menu to select the provided samples.
267    */
268   protected void createExamplesMenu(JMenuBar menuBar) {
269     String fqResourceName = FamilyTreeDemo.class.getPackage().getName().replace('.', '/') + "/samples/kennedy_clan.ged";
270 
271     URL resource = getClass().getResource("samples/kennedy_clan.ged");
272     if (resource == null) {
273       D.showError("Cannot load example files: missing resource " + fqResourceName + '\n' +
274           "Please ensure that your IDE recognizes \"*.ged\" files as resource files. \n" +
275           "Meanwhile you can load the sample files via the \"File/Load\" menu from the source folder of your distribution.");
276       return;
277     }
278 
279     final String name = resource.getFile();
280     final String dirName = name.substring(0, name.lastIndexOf('/'));
281 
282     final String[] dir = new File(dirName).list(new FilenameFilter() {
283       public boolean accept(File dir, String name) {
284         return name.toLowerCase().endsWith(".ged");
285       }
286     });
287 
288     if (dir == null) {
289       D.showError("Cannot load example files: " + dirName + " not found");
290       return;
291     }
292 
293     if (dir.length > 0) {
294       final JMenu menu = new JMenu("Example Graphs");
295       menuBar.add(menu);
296 
297       for (int i = 0; i < dir.length; i++) {
298         final String fileName = dir[i];
299         menu.add(new AbstractAction(fileName) {
300           public void actionPerformed(ActionEvent e) {
301             loadGedcom(dirName + System.getProperty("file.separator") + fileName);
302           }
303         });
304       }
305 
306       loadGedcom(dirName + System.getProperty("file.separator") + dir[0]);
307     }
308   }
309 
310   /**
311    * Loads a gedcom file with the provided filename into the editor.
312    * @param name the filename of the gedcom file to be read.
313    */
314   private void loadGedcom(String name) {
315     final Graph2D graph = view.getGraph2D();
316     if (graphHider != null) {
317       graphHider.unhideAll();
318     }
319     graph.clear();
320     GedcomHandler gh = new GedcomHandler();
321     GraphMLIOHandler delegate = new GraphMLIOHandler();
322     NodeMap nodeTypeMap = graph.createNodeMap();
323     graph.addDataProvider(FamilyTreeLayouter.DP_KEY_FAMILY_TYPE, nodeTypeMap);
324     delegate.getGraphMLHandler().addInputDataAcceptor("GedcomType", nodeTypeMap, KeyType.STRING);
325 
326     gh.setReaderDelegate(delegate);
327     try {
328       gh.read(graph, name);
329     } catch (IOException e1) {
330       D.show(e1);
331     }
332 
333     for (NodeCursor nodeCursor = graph.nodes(); nodeCursor.ok(); nodeCursor.next()) {
334       Node node = nodeCursor.node();
335       final NodeRealizer realizer = graph.getRealizer(node);
336       if (realizer.labelCount() > 0) {
337         final NodeLabel label = realizer.getLabel();
338         if (label.getFontSize() < PREFERRED_FONT_SIZE) {
339           label.setFontSize(PREFERRED_FONT_SIZE);
340 
341           if (label.getWidth() + 2*label.getDistance() > realizer.getWidth()) {
342             realizer.setWidth(label.getWidth() + 2*label.getDistance() + 8);
343           }
344         }
345         SmartNodeLabelModel labelModel = new SmartNodeLabelModel();
346         label.setLabelModel(labelModel);
347         label.setModelParameter(labelModel.getDefaultParameter());
348       }
349     }
350 
351     try {
352       getLayoutModule().start(view.getGraph2D());
353     } catch (Exception e1) {
354       D.show(e1);
355     }
356   }
357 
358 
359   /**
360    * Overrides the default method which creates the loadGedcom entry in the file menu to import a gedcom file
361    * rather than to loadGedcom a graph.
362    * @return A new instance of ImportAction
363    */
364   protected Action createLoadAction() {
365     return new ImportAction();
366   }
367 
368 
369   /**
370    * Action that loads a Gedcom file.
371    */
372   public class ImportAction extends AbstractAction {
373     JFileChooser chooser;
374 
375     public ImportAction() {
376       super("Load...");
377       chooser = null;
378     }
379 
380     public void actionPerformed(ActionEvent e) {
381       if (chooser == null) {
382         chooser = new JFileChooser();
383         chooser.setFileFilter(new FileFilter() {
384           public boolean accept(File f) {
385             return f.isDirectory() || f.getName().toLowerCase().endsWith(".ged");
386           }
387 
388           public String getDescription() {
389             return "Gedcom files";
390           }
391         });
392       }
393 
394       if (chooser.showOpenDialog(contentPane) == JFileChooser.APPROVE_OPTION) {
395         loadGedcom(chooser.getSelectedFile().toString());
396       }
397     }
398 
399 
400   }
401 
402   private FamilyTreeLayoutModule getLayoutModule() {
403     if (ftlm == null) {
404       ftlm = new FamilyTreeLayoutModule();       
405       ftlm.getOptionHandler().set("MALE_COLOR", DemoDefaults.DEFAULT_CONTRAST_COLOR);
406       ftlm.getOptionHandler().set("FEMALE_COLOR", DemoDefaults.DEFAULT_NODE_COLOR);      
407     }
408     return ftlm;
409   }
410 
411   /**
412    * Action to export the graph into a gedcom file
413    * NOTE: This action is not added to the toolbar
414    */
415   protected class ExportAction extends AbstractAction {
416 
417     private JFileChooser chooser;
418 
419     public ExportAction() {
420       super("Export");
421     }
422 
423     /**
424      * Invoked when an action occurs.
425      */
426     public void actionPerformed(ActionEvent e) {
427 
428       if (chooser == null) {
429         chooser = new JFileChooser();
430         chooser.setFileFilter(new FileFilter() {
431           public boolean accept(File f) {
432             return f.isDirectory()
433                 || f.getName().toLowerCase().endsWith(".ged");
434           }
435 
436           public String getDescription() {
437             return "Gedcom files";
438           }
439         });
440       }
441       if (chooser.showOpenDialog(contentPane) == JFileChooser.APPROVE_OPTION) {
442         String name = chooser.getSelectedFile().toString();
443         if (!name.toLowerCase().endsWith(".ged")) {
444           name = name + ".ged";
445         }
446 
447         GedcomHandler gh = new GedcomHandler();
448         final Graph2D graph = view.getGraph2D();
449 
450         // Write the graph using the GedcomHandler
451         try {
452           gh.write(graph, name);
453         } catch (IOException e1) {
454           D.show(e1);
455         }
456       }
457     }
458 
459   }
460 
461   //////////////////////////////////////// Optical improvements :-) /////////////////////////////////////////////////
462 
463   /**
464    * Initialize the node and edge style
465    */
466   protected void initialize() {
467 
468     // Use a BevelNodePainter for the Individuals
469     GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
470     Map implementationsMap = factory.createDefaultConfigurationMap();
471     ShinyPlateNodePainter spnp = new ShinyPlateNodePainter();
472     spnp.setDrawShadow(true);
473     implementationsMap.put(GenericNodeRealizer.Painter.class, spnp);
474     factory.addConfiguration("Individual", implementationsMap);
475 
476     // Use a BevelNodePainter with rounded corners for the families
477     implementationsMap = factory.createDefaultConfigurationMap();
478     BevelNodePainter painter = new BevelNodePainter();
479     painter.setRadius(10);
480     painter.setDrawShadow(true);
481     implementationsMap.put(GenericNodeRealizer.Painter.class, painter);
482     factory.addConfiguration("Family", implementationsMap);
483 
484     // Use a custom edge realizer
485     GenericEdgeRealizer.Factory edgeFactory = GenericEdgeRealizer.getFactory();
486     implementationsMap = edgeFactory.createDefaultConfigurationMap();
487     implementationsMap.put(GenericEdgeRealizer.Painter.class, new CustomEdgePainter());
488     edgeFactory.addConfiguration("Edge", implementationsMap);
489     GenericEdgeRealizer ger = new GenericEdgeRealizer();
490     ger.setConfiguration("Edge");
491     ger.setLineColor(new Color(0x808080));
492     ger.setLineType(LineType.LINE_2);
493     view.getGraph2D().setDefaultEdgeRealizer(ger);
494 
495     // Crossing/Bridges: Vertical edges over horizontal edges display gaps in horizontal edges
496     BridgeCalculator bc = new BridgeCalculator();
497     bc.setCrossingStyle(BridgeCalculator.CROSSING_STYLE_GAP);
498     bc.setCrossingMode(BridgeCalculator.CROSSING_MODE_ORDER_INDUCED);
499     ((DefaultGraph2DRenderer) view.getGraph2DRenderer()).setBridgeCalculator(bc);
500 
501     graphHider = new GraphHider(view.getGraph2D());
502 
503     
504   }
505 
506 
507   /**
508    * A custom EdgePainter implementation that draws the edge path 3D-ish and adds
509    * a drop shadow also. (see demo.view.realizer.GenericEdgePainterDemo)
510    */
511   static final class CustomEdgePainter extends GenericEdgePainter {
512 
513     protected GeneralPath adjustPath(EdgeRealizer context, BendList bends, GeneralPath path,
514                                      BridgeCalculator bridgeCalculator,
515                                      boolean selected) {
516       if (bridgeCalculator != null) {
517         GeneralPath p = new GeneralPath();
518         PathIterator pathIterator = bridgeCalculator.insertBridges(path.getPathIterator(null, 1.0d));
519         p.append(pathIterator, true);
520         return super.adjustPath(context, bends, p, bridgeCalculator, selected);
521       } else {
522         return super.adjustPath(context, bends, path, bridgeCalculator, selected);
523       }
524     }
525 
526 
527     protected void paintPath(EdgeRealizer context, BendList bends, GeneralPath path, Graphics2D gfx, boolean selected) {
528       Stroke s = gfx.getStroke();
529       Color oldColor = gfx.getColor();
530       if (s instanceof BasicStroke) {
531         Color c;
532         if (selected) {
533           initializeSelectionLine(context, gfx, selected);
534           c = gfx.getColor();
535         } else {
536           initializeLine(context, gfx, selected);
537           c = gfx.getColor();
538           gfx.setColor(new Color(128, 128, 128, 40));
539           gfx.translate(4, 4);
540           gfx.draw(path);
541           gfx.translate(-4, -4);
542         }
543         Color newC = selected ? Color.RED : c;
544         gfx.setColor(new Color(128 + newC.getRed() / 2, 128 + newC.getGreen() / 2, 128 + newC.getBlue() / 2));
545         gfx.translate(-1, -1);
546         gfx.draw(path);
547         gfx.setColor(new Color(newC.getRed() / 2, newC.getGreen() / 2, newC.getBlue() / 2));
548         gfx.translate(2, 2);
549         gfx.draw(path);
550         gfx.translate(-1, -1);
551         gfx.setColor(c);
552         gfx.draw(path);
553         gfx.setColor(oldColor);
554       } else {
555         gfx.draw(path);
556       }
557     }
558   }
559 }
560