[
  {
    "path": "LouisXIVfamily.txt",
    "content": "# This file is an example to show how to format a family.\n\n# Two lines represent an union:\nLouis XIV (M, birthday=1638-09-05, deathday=1715-09-01)\nMarie-Thérèse d'Autriche (F)\n# Indented lines after the union represent children\n\tLouis de France (id=Louis1661, M, birthday=1661-11-01, deathday=1711-04-14)\n\tMarie-Thérèse\\nde France (F, surname=la Petite Madame, birthday=1667, deathday=1672)\n\tPhilippe-Charles\\nde France (M, surname=Duc d'Anjou, birthday=1668-08-05)\n\n# Another union (2 parents + 3 children), father is one the previous union's children:\nLouis de France (id=Louis1661)\nMarie Anne\\nChristine\\nde Bavière (F)\n\tLouis de France (id=Louis1682, M, birthday=1682, deathday=1712-02-19, surname=duc de Bourgogne)\n\tPhilippe (M, birthday=1683, deathday=1746, surname=roi d'Espagne\\nsous le nom de\\nPhilippe V)\n\tCharles (M, birthday=1686-07-31)\n\n# When several persons have the same name, ids can be used to differentiate them.\nLouis de France (id=Louis1682)\nMarie-Adélaïde\\nde Savoie (F, deathday=1712-02-12)\n\tLouis XV (M, id=LouisXV, birthday=1710-02-15, deathday=1774-05-10)\n"
  },
  {
    "path": "README.md",
    "content": "familytreemaker\n===============\n\nThis program creates family tree graphs from simple text files.\n\nThe input file format is very simple, you describe persons of your family line\nby line, children just have to follow parents in the file. Persons can be\nrepeated as long as they keep the same name or id. An example is given in the\nfile `LouisXIVfamily.txt`.\n\n\nInstallation\n------------\n\nSimply clone the repo.\n\nThis script outputs a graph descriptor in DOT format. To make the image\ncontaining the graph, you will need a graph drawer such as [GraphViz] [1].\n\n[1]: http://www.graphviz.org/  \"GraphViz\"\n\nUsage\n-----\n\nThe sample family descriptor `LouisXIVfamily.txt` is here to show you the\nusage. Simply run:\n```\n$ ./familytreemaker.py -a 'Louis XIV' LouisXIVfamily.txt | dot -Tpng -o LouisXIVfamily.png\n```\nIt will generate the tree from the infos in `LouisXIVfamily.txt`, starting from\n*Louis XIV* and saving the image in `LouisXIVfamily.png`.\n\nYou can see the result:\n\n![result: LouisXIVfamily.png](/LouisXIVfamily.png)\n"
  },
  {
    "path": "familytreemaker.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n# Copyright (C) 2013 Adrien Vergé\n\n\"\"\"familytreemaker\n\nThis program creates family tree graphs from simple text files.\n\nThe input file format is very simple, you describe persons of your family line\nby line, children just have to follow parents in the file. Persons can be\nrepeated as long as they keep the same name or id. An example is given in the\nfile LouisXIVfamily.txt.\n\nThis script outputs a graph descriptor in DOT format. To make the image\ncontaining the graph, you will need a graph drawer such as GraphViz.\n\nFor instance:\n\n$ ./familytreemaker.py -a 'Louis XIV' LouisXIVfamily.txt | \\\n\tdot -Tpng -o LouisXIVfamily.png\n\nwill generate the tree from the infos in LouisXIVfamily.txt, starting from\nLouis XIV and saving the image in LouisXIVfamily.png.\n\n\"\"\"\n\n__author__ = \"Adrien Vergé\"\n__copyright__ = \"Copyright 2013, Adrien Vergé\"\n__license__ = \"GPL\"\n__version__ = \"1.0\"\n\nimport argparse\nimport random\nimport re\nimport sys\n\nclass Person:\n\t\"\"\"This class represents a person.\n\n\tCharacteristics:\n\t- name\t\t\treal name of the person\n\t- id\t\t\tunique ID to be distinguished in a dictionnary\n\t- attr\t\t\tattributes (e.g. gender, birth date...)\n\t- households\tlist of households this person belongs to\n\t- follow_kids\tboolean to tell the algorithm to display this person's\n\t\t\t\t\tdescendent or not\n\n\t\"\"\"\n\n\tdef __init__(self, desc):\n\t\tself.attr = {}\n\t\tself.parents = []\n\t\tself.households = []\n\n\t\tdesc = desc.strip()\n\t\tif '(' in desc and ')' in desc:\n\t\t\tself.name, attr = desc[0:-1].split('(')\n\t\t\tself.name = self.name.strip()\n\t\t\tattr = map(lambda x: x.strip(), attr.split(','))\n\t\t\tfor a in attr:\n\t\t\t\tif '=' in a:\n\t\t\t\t\tk, v = a.split('=')\n\t\t\t\t\tself.attr[k] = v\n\t\t\t\telse:\n\t\t\t\t\tself.attr[a] = True\n\t\telse:\n\t\t\tself.name = desc\n\n\t\tif 'id' in self.attr:\n\t\t\t  self.id = self.attr['id']\n\t\telse:\n\t\t\tself.id = re.sub('[^0-9A-Za-z]', '', self.name)\n\t\t\tif 'unique' in self.attr:\n\t\t\t\t  self.id += str(random.randint(100, 999))\n\n\t\tself.follow_kids = True\n\n\tdef __str__(self):\n\t\treturn self.name\n\n\tdef dump(self):\n\t\treturn\t'Person: %s (%s)\\n' % (self.name, str(self.attr)) + \\\n\t\t\t\t'  %d households' % len(self.households)\n\n\tdef graphviz(self):\n\t\tlabel = self.name\n\t\tif 'surname' in self.attr:\n\t\t\tlabel += '\\\\n« ' + str(self.attr['surname']) + '»'\n\t\tif 'birthday' in self.attr:\n\t\t\tlabel += '\\\\n' + str(self.attr['birthday'])\n\t\t\tif 'deathday' in self.attr:\n\t\t\t\tlabel += ' † ' + str(self.attr['deathday'])\n\t\telif 'deathday' in self.attr:\n\t\t\tlabel += '\\\\n† ' + str(self.attr['deathday'])\n\t\tif 'notes' in self.attr:\n\t\t\tlabel += '\\\\n' + str(self.attr['notes'])\n\t\topts = ['label=\"' + label + '\"']\n\t\topts.append('style=filled')\n\t\topts.append('fillcolor=' + ('F' in self.attr and 'bisque' or\n\t\t\t\t\t('M' in self.attr and 'azure2' or 'white')))\n\t\treturn self.id + '[' + ','.join(opts) + ']'\n\nclass Household:\n\t\"\"\"This class represents a household, i.e. a union of two person.\n\n\tThose two persons are listed in 'parents'. If they have children, they are\n\tlisted in 'kids'.\n\n\t\"\"\"\n\n\tdef __init__(self):\n\t\tself.parents = []\n\t\tself.kids = []\n\t\tself.id = 0\n\t\n\tdef __str__(self):\n\t\treturn\t'Family:\\n' + \\\n\t\t\t\t'\\tparents  = ' + ', '.join(map(str, self.parents)) + '\\n' \\\n\t\t\t\t'\\tchildren = ' + ', '.join(map(str, self.kids))\n\n\tdef isempty(self):\n\t\tif len(self.parents) == 0 and len(self.kids) == 0:\n\t\t\treturn True\n\t\treturn False\n\nclass Family:\n\t\"\"\"Represents the whole family.\n\n\t'everybody' contains all persons, indexed by their unique id\n\t'households' is the list of all unions (with or without children)\n\n\t\"\"\"\n\n\teverybody = {}\n\thouseholds = []\n\n\tinvisible = '[shape=circle,label=\"\",height=0.01,width=0.01]';\n\n\tdef add_person(self, string):\n\t\t\"\"\"Adds a person to self.everybody, or update his/her info if this\n\t\tperson already exists.\n\n\t\t\"\"\"\n\t\tp = Person(string)\n\t\tkey = p.id\n\n\t\tif key in self.everybody:\n\t\t\tself.everybody[key].attr.update(p.attr)\n\t\telse:\n\t\t\tself.everybody[key] = p\n\n\t\treturn self.everybody[key]\n\n\tdef add_household(self, h):\n\t\t\"\"\"Adds a union (household) to self.households, and updates the\n\t\tfamily members infos about this union.\n\n\t\t\"\"\"\n\t\tif len(h.parents) != 2:\n\t\t\tprint('error: number of parents != 2')\n\t\t\treturn\n\n\t\th.id = len(self.households)\n\t\tself.households.append(h)\n\n\t\tfor p in h.parents:\n\t\t\tif not h in p.households:\n\t\t\t\tp.households.append(h)\n\n\tdef find_person(self, name):\n\t\t\"\"\"Tries to find a person matching the 'name' argument.\n\n\t\t\"\"\"\n\t\t# First, search in ids\n\t\tif name in self.everybody:\n\t\t\treturn self.everybody[name]\n\t\t# Ancestor not found in 'id', maybe it's in the 'name' field?\n\t\tfor p in self.everybody.values():\n\t\t\tif p.name == name:\n\t\t\t\treturn p\n\t\treturn None\n\t\t\n\tdef populate(self, f):\n\t\t\"\"\"Reads the input file line by line, to find persons and unions.\n\n\t\t\"\"\"\n\t\th = Household()\n\t\twhile True:\n\t\t\tline = f.readline()\n\t\t\tif line == '': # end of file\n\t\t\t\tif not h.isempty():\n\t\t\t\t\tself.add_household(h)\n\t\t\t\tbreak\n\t\t\tline = line.rstrip()\n\t\t\tif line == '':\n\t\t\t\tif not h.isempty():\n\t\t\t\t\tself.add_household(h)\n\t\t\t\th = Household()\n\t\t\telif line[0] == '#':\n\t\t\t\tcontinue\n\t\t\telse:\n\t\t\t\tif line[0] == '\\t':\n\t\t\t\t\tp = self.add_person(line[1:])\n\t\t\t\t\tp.parents = h.parents\n\t\t\t\t\th.kids.append(p)\n\t\t\t\telse:\n\t\t\t\t\tp = self.add_person(line)\n\t\t\t\t\th.parents.append(p)\n\n\tdef find_first_ancestor(self):\n\t\t\"\"\"Returns the first ancestor found.\n\n\t\tA person is considered an ancestor if he/she has no parents.\n\n\t\tThis function is not very good, because we can have many persons with\n\t\tno parents, it will always return the first found. A better practice\n\t\twould be to return the one with the highest number of descendant.\n\t\t\n\t\t\"\"\"\n\t\tfor p in self.everybody.values():\n\t\t\tif len(p.parents) == 0:\n\t\t\t\treturn p\n\n\tdef next_generation(self, gen):\n\t\t\"\"\"Takes the generation N in argument, returns the generation N+1.\n\n\t\tGenerations are represented as a list of persons.\n\n\t\t\"\"\"\n\t\tnext_gen = []\n\n\t\tfor p in gen:\n\t\t\tif not p.follow_kids:\n\t\t\t\tcontinue\n\t\t\tfor h in p.households:\n\t\t\t\tnext_gen.extend(h.kids)\n\t\t\t\t# append mari/femme\n\n\t\treturn next_gen\n\n\tdef get_spouse(household, person):\n\t\t\"\"\"Returns the spouse or husband of a person in a union.\n\n\t\t\"\"\"\n\t\treturn\thousehold.parents[0] == person \\\n\t\t\t\tand household.parents[1] or household.parents[0]\n\n\tdef display_generation(self, gen):\n\t\t\"\"\"Outputs an entire generation in DOT format.\n\n\t\t\"\"\"\n\t\t# Display persons\n\t\tprint('\\t{ rank=same;')\n\n\t\tprev = None\n\t\tfor p in gen:\n\t\t\tl = len(p.households)\n\n\t\t\tif prev:\n\t\t\t\tif l <= 1:\n\t\t\t\t\tprint('\\t\\t%s -> %s [style=invis];' % (prev, p.id))\n\t\t\t\telse:\n\t\t\t\t\tprint('\\t\\t%s -> %s [style=invis];'\n\t\t\t\t\t\t  % (prev, Family.get_spouse(p.households[0], p).id))\n\n\t\t\tif l == 0:\n\t\t\t\tprev = p.id\n\t\t\t\tcontinue\n\t\t\telif len(p.households) > 2:\n\t\t\t\traise Exception('Person \"' + p.name + '\" has more than 2 ' +\n\t\t\t\t\t\t\t\t'spouses/husbands: drawing this is not ' +\n\t\t\t\t\t\t\t\t'implemented')\n\n\t\t\t# Display those on the left (if any)\n\t\t\tfor i in range(0, int(l/2)):\n\t\t\t\th = p.households[i]\n\t\t\t\tspouse = Family.get_spouse(h, p)\n\t\t\t\tprint('\\t\\t%s -> h%d -> %s;' % (spouse.id, h.id, p.id))\n\t\t\t\tprint('\\t\\th%d%s;' % (h.id, Family.invisible))\n\n\t\t\t# Display those on the right (at least one)\n\t\t\tfor i in range(int(l/2), l):\n\t\t\t\th = p.households[i]\n\t\t\t\tspouse = Family.get_spouse(h, p)\n\t\t\t\tprint('\\t\\t%s -> h%d -> %s;' % (p.id, h.id, spouse.id))\n\t\t\t\tprint('\\t\\th%d%s;' % (h.id, Family.invisible))\n\t\t\t\tprev = spouse.id\n\t\tprint('\\t}')\n\n\t\t# Display lines below households\n\t\tprint('\\t{ rank=same;')\n\t\tprev = None\n\t\tfor p in gen:\n\t\t\tfor h in p.households:\n\t\t\t\tif len(h.kids) == 0:\n\t\t\t\t\tcontinue\n\t\t\t\tif prev:\n\t\t\t\t\tprint('\\t\\t%s -> h%d_0 [style=invis];' % (prev, h.id))\n\t\t\t\tl = len(h.kids)\n\t\t\t\tif l % 2 == 0:\n\t\t\t\t\t# We need to add a node to keep symmetry\n\t\t\t\t\tl += 1\n\t\t\t\tprint('\\t\\t' + ' -> '.join(map(lambda x: 'h%d_%d' % (h.id, x), range(l))) + ';')\n\t\t\t\tfor i in range(l):\n\t\t\t\t\tprint('\\t\\th%d_%d%s;' % (h.id, i, Family.invisible))\n\t\t\t\t\tprev = 'h%d_%d' % (h.id, i)\n\t\tprint('\\t}')\n\n\t\tfor p in gen:\n\t\t\tfor h in p.households:\n\t\t\t\tif len(h.kids) > 0:\n\t\t\t\t\tprint('\\t\\th%d -> h%d_%d;'\n\t\t\t\t\t      % (h.id, h.id, int(len(h.kids)/2)))\n\t\t\t\t\ti = 0\n\t\t\t\t\tfor c in h.kids:\n\t\t\t\t\t\tprint('\\t\\th%d_%d -> %s;'\n\t\t\t\t\t\t      % (h.id, i, c.id))\n\t\t\t\t\t\ti += 1\n\t\t\t\t\t\tif i == len(h.kids)/2:\n\t\t\t\t\t\t\ti += 1\n\n\tdef output_descending_tree(self, ancestor):\n\t\t\"\"\"Outputs the whole descending family tree from a given ancestor,\n\t\tin DOT format.\n\n\t\t\"\"\"\n\t\t# Find the first households\n\t\tgen = [ancestor]\n\n\t\tprint('digraph {\\n' + \\\n\t\t      '\\tnode [shape=box];\\n' + \\\n\t\t      '\\tedge [dir=none];\\n')\n\n\t\tfor p in self.everybody.values():\n\t\t\tprint('\\t' + p.graphviz() + ';')\n\t\tprint('')\n\n\t\twhile gen:\n\t\t\tself.display_generation(gen)\n\t\t\tgen = self.next_generation(gen)\n\n\t\tprint('}')\n\ndef main():\n\t\"\"\"Entry point of the program when called as a script.\n\n\t\"\"\"\n\t# Parse command line options\n\tparser = argparse.ArgumentParser(description=\n\t\t\t 'Generates a family tree graph from a simple text file')\n\tparser.add_argument('-a', dest='ancestor',\n\t\t\t\t\t\thelp='make the family tree from an ancestor (if '+\n\t\t\t\t\t\t'omitted, the program will try to find an ancestor)')\n\tparser.add_argument('input', metavar='INPUTFILE',\n\t\t\t\t\t\thelp='the formatted text file representing the family')\n\targs = parser.parse_args()\n\n\t# Create the family\n\tfamily = Family()\n\n\t# Populate the family\n\tf = open(args.input, 'r', encoding='utf-8')\n\tfamily.populate(f)\n\tf.close()\n\n\t# Find the ancestor from whom the tree is built\n\tif args.ancestor:\n\t\tancestor = family.find_person(args.ancestor)\n\t\tif not ancestor:\n\t\t\traise Exception('Cannot find person \"' + args.ancestor + '\"')\n\telse:\n\t\tancestor = family.find_first_ancestor()\n\n\t# Output the graph descriptor, in DOT format\n\tfamily.output_descending_tree(ancestor)\n\nif __name__ == '__main__':\n\tmain()\n"
  }
]