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