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.iohandler;
29  
30  import y.base.DataAcceptor;
31  import y.base.DataProvider;
32  import y.base.Node;
33  import y.layout.genealogy.FamilyTreeLayouter;
34  import y.view.EdgeRealizer;
35  import y.view.Graph2D;
36  import y.view.NodeLabel;
37  import y.view.NodeRealizer;
38  import y.view.SmartNodeLabelModel;
39  import y.view.YLabel;
40  
41  import java.awt.Color;
42  import java.util.HashMap;
43  import java.util.Map;
44  import java.util.Set;
45  
46  /**
47   * Gets the information of a GEDCOM line and builds a graph with it.
48   * <p/>
49   * This class provides several callback methods to customize the handling of GEDCOM tags. In this implementation, the
50   * tags INDI, FAM, WIFE, HUSB and CHIL are used to get the graph structure and the tags NAME, BIRT, DEAT and DATE are
51   * used to create labels with the according information.
52   */
53  public class GedcomInputHandlerImpl implements GedcomInputHandler {
54    public static final Color DEFAULT_COLOR_FEMALE = new Color(204, 204, 255);
55    public static final Color DEFAULT_COLOR_MALE = new Color(255, 204, 153);
56    private static final String LABEL_CONFIG = "CroppingLabel";
57  
58    private final Graph2D graph;
59    private final Map ids2nodes;
60    private Node currentNode;
61    private NodeLabel currentNodeLabel;
62  
63    /**
64     * Initializes a new <code>GedcomInputHandlerImpl</code> for the given
65     * graph structure.
66     */
67    public GedcomInputHandlerImpl(Graph2D graph) {
68      this.graph = graph;
69      ids2nodes = new HashMap();
70    }
71  
72    /**
73     * Returns the graph structure for which this handler was created.
74     * @return the graph structure for which this handler was created.
75     */
76    protected Graph2D getGraph() {
77      return graph;
78    }
79  
80    /**
81     * Reacts to the beginning of the GEDCOM file. By default nothing happens.
82     */
83    public void handleStartDocument() {
84    }
85  
86    /**
87     * Reacts to the end of the GEDCOM file. By default nothing happens.
88     */
89    public void handleEndDocument() {
90    }
91  
92    /**
93     * Processes the given information according to the value of the tag.
94     *
95     * @param id    the id from the GEDCOM line (might be <code>null</code>)
96     * @param tag   the tag from the GEDCOM line
97     * @param value the value from the GEDCOM line (might be <code>null</code>)
98     */
99    public void handleStartTag(int level, String id, String tag, String value) {
100     if ("INDI".equals(tag)) {
101       handleIndividualTag(graph, level, id);
102     } else if ("FAM".equals(tag)) {
103       handleFamilyTag(graph, level, id);
104     } else if ("NAME".equals(tag)) {
105       handleNameTag(graph, currentNode, level, value);
106     } else if ("SEX".equals(tag)) {
107       handleSexTag(graph, currentNode, level, value);
108     } else if ("BIRT".equals(tag)) {
109       handleBirthTag(graph, currentNode, level, value);
110     } else if ("DEAT".equals(tag)) {
111       handleDeathTag(graph, currentNode, level, value);
112     } else if ("DATE".equals(tag)) {
113       handleDateTag(graph, currentNode, currentNodeLabel, level, value);
114     } else if ("HUSB".equals(tag)) {
115       handleHusbandTag(graph, currentNode, level, value);
116     } else if ("WIFE".equals(tag)) {
117       handleWifeTag(graph, currentNode, level, value);
118     } else if ("CHIL".equals(tag)) {
119       handleChildTag(graph, currentNode, level, value);
120     } else {
121       handleMiscTag(graph, currentNode, currentNodeLabel, level, id, tag, value);
122     }
123   }
124 
125   /**
126    * Cleans up when a level ends.
127    *
128    * @param tag the tag of the level that ends
129    */
130   public void handleEndTag(int level, String tag) {
131     handleEndTag(graph, currentNode, currentNodeLabel, level, tag);
132     if ("INDI".equals(tag) || "FAM".equals(tag)) {
133       currentNode = null;
134     } else if ("NAME".equals(tag) || "BIRT".equals(tag) || "DEAT".equals(tag) || "MARR".equals(tag)
135         || "DATE".equals(tag)) {
136       currentNodeLabel = null;
137     }
138   }
139   
140   protected void handleEndTag(Graph2D graph, Node node, NodeLabel label, int level, String tag) {
141   }
142 
143   /**
144    * Starts building an individual node in the graph.
145    *
146    * @param graph the graph that is built
147    * @param id    the id of the individual
148    */
149   protected void handleIndividualTag(Graph2D graph, int level, String id) {
150     currentNode = (Node) ids2nodes.get(id.trim());
151     if (currentNode == null) {
152       currentNode = createIndividualNode(graph);
153       ids2nodes.put(id.trim(), currentNode);
154     }
155   }
156 
157   /**
158    * Creates a node for an individual in the family tree.
159    * <p/>
160    * To customize the representation of individuals,
161    * this method may be overwritten.
162    *
163    * @param graph the graph to be built
164    * @return the new individual node
165    */
166   protected Node createIndividualNode(Graph2D graph) {
167     return graph.createNode(createIndividualNodeRealizer(graph));
168   }
169 
170   /**
171    * Creates the visual representation of an individual.
172    * <p/>
173    * To customize the visual representation of individuals,
174    * this method may be overwritten.
175    *
176    * @param graph the graph to be built
177    * @return the node realizer representing an individual
178    */
179   protected NodeRealizer createIndividualNodeRealizer(Graph2D graph) {
180     return graph.getDefaultNodeRealizer().createCopy();
181   }
182 
183   /**
184    * Starts building a family node in the graph.
185    *
186    * @param graph the graph to be built
187    * @param id    the id of the family
188    */
189   protected void handleFamilyTag(Graph2D graph, int level, String id) {
190     currentNode = (Node) ids2nodes.get(id.trim());
191     if (currentNode == null) {
192       currentNode = createFamilyNode(graph);
193       DataProvider dp = graph.getDataProvider(FamilyTreeLayouter.DP_KEY_FAMILY_TYPE);
194       if (dp instanceof DataAcceptor) {
195         ((DataAcceptor) dp).set(currentNode, FamilyTreeLayouter.TYPE_FAMILY);
196       }
197       ids2nodes.put(id.trim(), currentNode);
198     }
199   }
200 
201   /**
202    * Creates a node for a family in the family tree.
203    * <p/>
204    * To customize the representation of families,
205    * this method may be overwritten.
206    *
207    * @param graph the graph to be built
208    * @return the new family node
209    */
210   protected Node createFamilyNode(Graph2D graph) {
211     return graph.createNode(createFamilyNodeRealizer(graph));
212   }
213 
214   /**
215    * Creates the visual representation of a family.
216    * <p/>
217    * To customize the visual representation of families,
218    * this method may be overwritten.
219    *
220    * @param graph the graph to be built
221    * @return the node realizer representing a family
222    */
223   protected NodeRealizer createFamilyNodeRealizer(Graph2D graph) {
224     return graph.getDefaultNodeRealizer().createCopy();
225   }
226 
227   /**
228    * Starts adding a name to an individual.
229    * <p/>
230    * To customize the representation of the name this method may be overwritten.
231    *
232    * @param graph the graph to be built
233    * @param node  the currently changed node the label belongs to
234    * @param value the name text
235    */
236   protected void handleNameTag(Graph2D graph, Node node, int level, String value) {
237     if (node != null) {
238       final String name = value.replaceFirst("/", "\n").replaceAll("/", " ").trim();
239       final NodeLabel label = createLabel(name);
240       final SmartNodeLabelModel model = new SmartNodeLabelModel();
241       label.setLabelModel(model,
242           model.createDiscreteModelParameter(SmartNodeLabelModel.POSITION_TOP));
243       graph.getRealizer(node).setLabel(label);
244     }
245   }
246 
247   /**
248    * Creates the label for the name of an individual.
249    * <p/>
250    * To customize the representation of the name this method may be overwritten.
251    *
252    * @param name the name text
253    * @return the label with the name
254    */
255   protected NodeLabel createLabel(String name) {
256     currentNodeLabel = new NodeLabel(name);
257     return currentNodeLabel;
258   }
259 
260   /**
261    * Changes the color of the current node and adds an entry in the <code>DataProvider</code> for the {@link
262    * FamilyTreeLayouter} according to the sex of the individual.
263    * <p/>
264    * To customize the reaction to this tag, this method may be overwritten.
265    *
266    * @param graph the graph to be built
267    * @param node  the currently changed node
268    * @param value the indication of the sex (F=female, M=male)
269    */
270   protected void handleSexTag(Graph2D graph, Node node, int level, String value) {
271     if (node != null) {
272       String type;
273       Color color;
274       if ("F".equals(value)) {
275         type = FamilyTreeLayouter.TYPE_FEMALE;
276         color = DEFAULT_COLOR_FEMALE;
277       } else {
278         type = FamilyTreeLayouter.TYPE_MALE;
279         color = DEFAULT_COLOR_MALE;
280       }
281       final NodeRealizer realizer = graph.getRealizer(node);
282       realizer.setFillColor(color);
283       realizer.setLineColor(null);
284       DataProvider dp = graph.getDataProvider(FamilyTreeLayouter.DP_KEY_FAMILY_TYPE);
285       if (dp instanceof DataAcceptor) {
286         ((DataAcceptor) dp).set(node, type);
287       }
288     }
289   }
290 
291   /**
292    * Starts creating a label for the date of birth of an individual.
293    * <p/>
294    * To customize the representation to the date of birth, this method may be overwritten.
295    *
296    * @param graph the graph to be built
297    * @param node  the currently changed node
298    * @param value the value of the GEDCOM line
299    */
300   protected void handleBirthTag(Graph2D graph, Node node, int level, String value) {
301     if (node != null) {
302       final NodeLabel label = createLabel("* ");
303       Set configurations = NodeLabel.getFactory().getAvailableConfigurations();
304       if (configurations.contains(LABEL_CONFIG)) {
305         label.setConfiguration(LABEL_CONFIG);
306         label.setAutoSizePolicy(YLabel.AUTOSIZE_NONE);
307         label.setContentSize(graph.getRealizer(node).getWidth() * 0.5, 20.0);
308       }
309       label.setAlignment(YLabel.ALIGN_LEFT);
310       final SmartNodeLabelModel model = new SmartNodeLabelModel();
311       label.setLabelModel(model,
312           model.createDiscreteModelParameter(SmartNodeLabelModel.POSITION_BOTTOM_LEFT));
313       graph.getRealizer(node).addLabel(label);
314     }
315   }
316 
317   /**
318    * Starts creating a label for the date of death of an individual.
319    * <p/>
320    * To customize the representation of the date of death, this method may be overwritten.
321    *
322    * @param graph the graph to be built
323    * @param node  the currently changed node
324    * @param value the value of the GEDCOM line
325    */
326   protected void handleDeathTag(Graph2D graph, Node node, int level, String value) {
327     if (node != null) {
328       final NodeLabel label = createLabel("\u271D ");
329       Set configurations = NodeLabel.getFactory().getAvailableConfigurations();
330       if (configurations.contains(LABEL_CONFIG)) {
331         label.setConfiguration(LABEL_CONFIG);
332         label.setAutoSizePolicy(YLabel.AUTOSIZE_NONE);
333         label.setContentSize(graph.getRealizer(node).getWidth() * 0.5, 20.0);
334       }
335       label.setAlignment(YLabel.ALIGN_RIGHT);
336       final SmartNodeLabelModel model = new SmartNodeLabelModel();
337       label.setLabelModel(model,
338           model.createDiscreteModelParameter(SmartNodeLabelModel.POSITION_BOTTOM_RIGHT));
339       graph.getRealizer(node).addLabel(label);
340     }
341   }
342 
343   /**
344    * Adds the date text to a currently active label.
345    * <p/>
346    * This implementation writes the date as it comes from the GEDCOM file. To customize the representation of dates,
347    * this method may be overwritten.
348    *
349    * @param graph the graph to be built
350    * @param node  the currently changed node
351    * @param label the currently changed label
352    * @param value the date text
353    */
354   protected void handleDateTag(Graph2D graph, Node node, NodeLabel label, int level, String value) {
355     if (label != null) {
356       label.setText(label.getText() + value);
357     }
358   }
359 
360   /**
361    * Creates an edge between the wife node identified by the given id and the currently active family node.
362    * <p/>
363    * To customize the creation of this edge, this method may be overwritten.
364    *
365    * @param graph the graph to be built
366    * @param node  the currently changed family node
367    * @param id    the id of the wife
368    */
369   protected void handleWifeTag(Graph2D graph, Node node, int level, String id) {
370     if (node != null && isValidID(id)) {
371       Node wife = getNodeByID(id);
372       if (wife == null && id.length() > 0) {
373         wife = createIndividualNode(graph);
374         ids2nodes.put(id, wife);
375       }
376       graph.createEdge(wife, node, createWifeFamilyEdgeRealizer(graph));
377     }
378   }
379 
380   /**
381    * Creates a realizer for an edge between the wife node and the family node.
382    * <p/>
383    * To customize the representation of the edge from the wife to the family node, this method may be overwritten.
384    *
385    * @param graph the graph to be built
386    * @return the edge realizer
387    */
388   protected EdgeRealizer createWifeFamilyEdgeRealizer(Graph2D graph) {
389     return graph.getDefaultEdgeRealizer().createCopy();
390   }
391 
392   /**
393    * Creates an edge between the husband node identified by the given id and the currently active family node.
394    * <p/>
395    * To customize the creation of this edge, this method may be overwritten.
396    *
397    * @param graph the graph to be built
398    * @param node  the currently changed family node
399    * @param id    the id of the husband
400    */
401   protected void handleHusbandTag(Graph2D graph, Node node, int level, String id) {
402     if (node != null && isValidID(id)) {
403       Node husband = getNodeByID(id);
404       if (husband == null) {
405         husband = createIndividualNode(graph);
406         ids2nodes.put(id, husband);
407       }
408       graph.createEdge(husband, node, createHusbandFamilyEdgeRealizer(graph));
409     }
410   }
411 
412   /**
413    * Creates a realizer for an edge between the husband node and the family node.
414    * <p/>
415    * To customize the representation of the edge from the husband to the family node, this method may be overwritten.
416    *
417    * @param graph the graph to be built
418    * @return the edge realizer
419    */
420   protected EdgeRealizer createHusbandFamilyEdgeRealizer(Graph2D graph) {
421     return graph.getDefaultEdgeRealizer().createCopy();
422   }
423 
424   /**
425    * Creates an edge between the child node identified by the given id and the currently active family node.
426    * <p/>
427    * To customize the creation of this edge, this method may be overwritten.
428    *
429    * @param graph the graph to be built
430    * @param node  the currently changed family node
431    * @param id    the id of the child
432    */
433   protected void handleChildTag(Graph2D graph, Node node, int level, String id) {
434     if (node != null && isValidID(id)) {
435       Node child = getNodeByID(id);
436       if (child == null && id.length() > 0) {
437         child = createIndividualNode(graph);
438         ids2nodes.put(id, child);
439       }
440       graph.createEdge(node, child, createFamilyChildEdgeRealizer(graph));
441     }
442   }
443 
444   /**
445    * Creates a realizer for an edge between the family node and the child node.
446    * <p/>
447    * To customize the representation of the edge from the family to the child node, this method may be overwritten.
448    *
449    * @param graph the graph to be built
450    * @return the edge realizer
451    */
452   protected EdgeRealizer createFamilyChildEdgeRealizer(Graph2D graph) {
453     return graph.getDefaultEdgeRealizer().createCopy();
454   }
455 
456   /**
457    * Handles miscellaneous GEDCOM tags.
458    * <p/>
459    * By default nothing happens. To handle a custom range of tags, overwrite this method.
460    *
461    * @param graph the graph to be built
462    * @param node  the currently changed node
463    * @param label the currently changed label
464    * @param id    the id from the GEDCOM line (might be <code>null</code>)
465    * @param tag   the tag from the GEDCOM line
466    * @param value the value from the GEDCOM line (might be <code>null</code>)
467    */
468   protected void handleMiscTag(Graph2D graph, Node node, NodeLabel label,
469                                int level, String id, String tag, String value) {
470   }
471 
472   /**
473    * Returns the node of the specified id.
474    *
475    * @param id the id of the node to return
476    * @return the node of the specified id
477    */
478   public Node getNodeByID(String id) {
479     return (Node) ids2nodes.get(id);
480   }
481 
482   /**
483    * Validates if the given id is a GEDCOM id. The id has to start and end with an @.
484    *
485    * @param id the id to be checked
486    * @return <code>true</code>, if the id is valid, <code>false</code> otherwise
487    */
488   static boolean isValidID(String id) {
489     return id != null && id.startsWith("@") && id.endsWith("@");
490   }
491 }