1 /*
2  * Copyright 2009 Alexander Curtis <alex@logicmill.com>
3  * This file is part of GEDmill - A family history website creator
4  *
5  * GEDmill is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * GEDmill is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with GEDmill.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 using System.Collections.Generic;
20 using System.Drawing;
21 using System.Drawing.Imaging;
22 using System.IO;
23 using GDModel;
24 using GKCore.Logging;
25 
26 namespace GEDmill.MiniTree
27 {
28     /*
29      * In this file, Parents are the top generation, siblings are the middle generation (including the subject of the tree),
30      * and children are the bottom generation. Subject's spouses come in middle generation.
31      * The data structure looks like this:
32      *    ________   _____________________________________________________________________________________________________   ________
33      *   | father | | siblings                                                                                            | | mother |
34      *   |        |-|  _______   _______   ________   _________________   ______   _________________   _______   _______  |-|        |
35      *   |________| | |sibling| |sibling| |spouse/ | |children         | |spouse| |children         | |subject| |sibling| | |________|
36      *              | |       |-|       |-|subject |-|  _____   _____  |-|      |-|  _____   _____  |-|/spouse|-|       | |
37      *              | |_______| |_______| |________| | |child| |child| | |______| | |child| |child| | |_______| |_______| |
38      *              |                                | |_____|-|_____|--------------|_____|-|_____| |                     |
39      *              |                                |_________________|          |_________________|                     |
40      *              |_____________________________________________________________________________________________________|
41      */
42 
43     /// <summary>
44     /// Class that calculates and draws a mini tree diagram
45     /// </summary>
46     public class TreeDrawer
47     {
48         private static readonly ILogger fLogger = LogManager.GetLogger(GMConfig.LOG_FILE, GMConfig.LOG_LEVEL, typeof(TreeDrawer).Name);
49 
50         /// <summary>
51         /// Data structure containing the information to put in the boxes in the tree
52         /// </summary>
53         private class CBoxText
54         {
55             // Individual's name
56             public string Name;
57 
58             // Dates to put in the box
59             public string Date;
60 
61             // Individual's first name
62             public string FirstName;
63 
64             // Individual's surname
65             public string Surname;
66 
67             // Whether the information is private
68             public bool Concealed;
69 
70 
CBoxText(GDMIndividualRecord ir)71             public CBoxText(GDMIndividualRecord ir)
72             {
73                 FirstName = "";
74                 Surname = "";
75                 Concealed = !GMHelper.GetVisibility(ir);
76                 if (Concealed && !GMConfig.Instance.UseWithheldNames) {
77                     FirstName = "";
78                     Surname = Name = GMConfig.Instance.ConcealedName;
79                 } else {
80                     var irName = ir.GetPrimaryFullName();
81                     if (irName != "") {
82                         Name = GMHelper.CapitaliseName(irName, ref FirstName, ref Surname);
83                     } else {
84                         FirstName = "";
85                         Surname = Name = GMConfig.Instance.UnknownName;
86                     }
87                 }
88 
89                 Date = Concealed ? string.Empty : GMHelper.GetLifeDatesStr(ir);
90             }
91         }
92 
93         // Total size of the tree
94         private SizeF fSizeTotal;
95 
96         // Reference to the global gedcom data
97         private GDMTree fTree;
98 
99 
100         // Returns the height of the whole tree diagram.
101         public int Height
102         {
103             get { return (int)fSizeTotal.Height; }
104         }
105 
106 
TreeDrawer(GDMTree tree)107         public TreeDrawer(GDMTree tree)
108         {
109             fTree = tree;
110         }
111 
112         // This is the main tree drawing method.
113         // irSubject is the individual for whom the tree is based.
114         // nTargeWidth is the width below which the layout is free to use up space to produce a nice tree.
CreateMiniTree(Paintbox paintbox, GDMIndividualRecord ir, string fileName, int targetWidth, ImageFormat imageFormat)115         public List<MiniTreeMap> CreateMiniTree(Paintbox paintbox, GDMIndividualRecord ir, string fileName, int targetWidth, ImageFormat imageFormat)
116         {
117             // First calculate size required for tree, by iterating through individuals and building a data structure
118             MiniTreeGroup mtgParent = CreateDataStructure(ir);
119 
120             // For each individual calculate size of box required for display using helper function
121             // There must be a better way to get a graphics:
122             Bitmap bmp = new Bitmap(1, 1, PixelFormat.Format24bppRgb);
123             Graphics g = Graphics.FromImage(bmp);
124             Font f = paintbox.Font;
125 
126             // Record what font windows actually used, in case it chose a different one
127             GMConfig.Instance.TreeFontName = f.Name;
128             GMConfig.Instance.TreeFontSize = f.Size;
129 
130             // Recursively calculate sizes of other groups
131             mtgParent.CalculateSize(g, f);
132 
133             g.Dispose();
134             bmp.Dispose();
135 
136             // Now calculate sizes of each row
137             // Total width includes irSubject, their spouses and their siblings.
138             // Total height is always three generations
139 
140             // Now calculate how best to position each generation
141             // Calculate the width of each generation
142             // There are three cases : frParents widest, siblings widest, children widest
143             // Plus two aims : minimise total width, get offspring centred under frParents.
144             // If nTargetWidth is exceeded simply because of number of individuals in one row, that
145             // row's width becomes the new target width.
146             // If nTargetWidth is exceeded otherwise, minimising total width becomes the priority
147             mtgParent.CalculateLayout(0f, 0f);
148             mtgParent.Compress();
149 
150             RectangleF rect = mtgParent.GetExtent();
151             fSizeTotal = new SizeF(rect.Width, rect.Height);
152             mtgParent.Translate(-rect.Left, -rect.Top);
153 
154             // Calculate offset for each row
155             // Can't do this so create a new bitmap: bmp.Width = totalSize.Width;
156             // Can't do this so create a new bitmap: bmp.Height = totalSize.Height;
157             int nTotalWidth = (int)(fSizeTotal.Width + 1.0f);
158             int nTotalHeight = (int)(fSizeTotal.Height + 1.0f);
159             bmp = new Bitmap(nTotalWidth, nTotalHeight, PixelFormat.Format32bppArgb);
160             g = Graphics.FromImage(bmp);
161 
162             // Do background fill
163             if (GMConfig.Instance.FakeMiniTreeTransparency && paintbox.BrushFakeTransparency != null) {
164                 g.FillRectangle(paintbox.BrushFakeTransparency, 0, 0, nTotalWidth, nTotalHeight);
165             } else if (imageFormat == ImageFormat.Gif && paintbox.BrushBgGif != null) {
166                 g.FillRectangle(paintbox.BrushBgGif, 0, 0, nTotalWidth, nTotalHeight);
167             }
168 
169             List<MiniTreeMap> alMap = new List<MiniTreeMap>();
170             mtgParent.DrawBitmap(paintbox, g, alMap);
171 
172             // Save the bitmap
173             fLogger.WriteInfo("Saving mini tree as " + fileName);
174 
175             if (File.Exists(fileName)) {
176                 // Delete any current file
177                 File.SetAttributes(fileName, FileAttributes.Normal);
178                 File.Delete(fileName);
179             }
180 
181             // Save using FileStream to try to avoid crash (only seen by customers)
182             FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write);
183             bmp.Save(fs, imageFormat);
184             fs.Close();
185 
186             g.Dispose();
187             bmp.Dispose();
188 
189             // For gifs we need to reload and set transparency colour
190             if (imageFormat == ImageFormat.Gif && !GMConfig.Instance.FakeMiniTreeTransparency) {
191                 var imageGif = Image.FromFile(fileName);
192                 var colorPalette = imageGif.Palette;
193 
194                 // Creates a new GIF image with a modified colour palette
195                 if (colorPalette != null) {
196                     // Create a new 8 bit per pixel image
197                     Bitmap bm = new Bitmap(imageGif.Width, imageGif.Height, PixelFormat.Format8bppIndexed);
198 
199                     // Get it's palette
200                     ColorPalette colorpaletteNew = bm.Palette;
201 
202                     // Copy all the entries from the old palette removing any transparency
203                     int n = 0;
204                     foreach (Color c in colorPalette.Entries) {
205                         colorpaletteNew.Entries[n++] = Color.FromArgb(255, c);
206                     }
207 
208                     // Now to copy the actual bitmap data
209                     // Lock the source and destination bits
210                     BitmapData src = ((Bitmap)imageGif).LockBits(new Rectangle(0, 0, imageGif.Width, imageGif.Height), ImageLockMode.ReadOnly, imageGif.PixelFormat);
211                     BitmapData dst = bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.WriteOnly, bm.PixelFormat);
212 
213                     // Uses pointers so we need unsafe code.
214                     // The project is also compiled with /unsafe
215                     byte backColor = 0;
216                     unsafe {
217                         backColor = ((byte*)src.Scan0.ToPointer())[0]; // Assume transparent colour appears as first pixel.
218 
219                         byte* src_ptr = ((byte*)src.Scan0.ToPointer());
220                         byte* dst_ptr = ((byte*)dst.Scan0.ToPointer());
221                         // May be useful: System.Runtime.InteropServices.Marshal.Copy(IntPtr source, byte[], destination, int start, int length)
222                         // May be useful: System.IO.MemoryStream ms = new System.IO.MemoryStream(src_ptr);
223                         int width = imageGif.Width;
224                         int src_stride = src.Stride - width;
225                         int dst_stride = dst.Stride - width;
226                         for (int y = 0; y < imageGif.Height; y++) {
227                             // Can't convert IntPtr to byte[]: Buffer.BlockCopy( src_ptr, 0, dst_ptr, 0, width );
228                             int x = width;
229                             while (x-- > 0) {
230                                 *dst_ptr++ = *src_ptr++;
231                             }
232                             src_ptr += src_stride;
233                             dst_ptr += dst_stride;
234                         }
235                     }
236 
237                     // Set the newly selected transparency
238                     colorpaletteNew.Entries[backColor] = Color.FromArgb(0, Color.Magenta);
239 
240                     // Re-insert the palette
241                     bm.Palette = colorpaletteNew;
242 
243                     // All done, unlock the bitmaps
244                     ((Bitmap)imageGif).UnlockBits(src);
245                     bm.UnlockBits(dst);
246 
247                     imageGif.Dispose();
248 
249                     // Set the new image in place
250                     imageGif = bm;
251                     colorPalette = imageGif.Palette;
252 
253                     fLogger.WriteInfo("Re-saving mini gif as " + fileName);
254 
255                     imageGif.Save(fileName, imageFormat);
256                 }
257             }
258 
259             return alMap;
260         }
261 
262         // Calculate size required for tree by iterating through individuals and building a data structure.
CreateDataStructure(GDMIndividualRecord irSubject)263         protected MiniTreeGroup CreateDataStructure(GDMIndividualRecord irSubject)
264         {
265             // Add subject's frParents
266             GDMFamilyRecord familyParents = fTree.GetParentsFamily(irSubject);
267             GDMIndividualRecord husband, wife;
268             fTree.GetSpouses(familyParents, out husband, out wife);
269 
270             MiniTreeGroup mtgParents = new MiniTreeGroup();
271             MiniTreeIndividual mtiFather = null;
272             if (familyParents != null) {
273                 mtiFather = AddToGroup(husband, mtgParents);
274             }
275 
276             // Create a group for the subject and their siblings.
277             var mtgSiblings = new MiniTreeGroup();
278 
279             // Keeps count of subject's siblings (including subject)
280             int siblings = 0;
281 
282             // Keeps track of last added sibling, to hook up to next added sibling.
283             MiniTreeIndividual mtiRightmostSibling = null;
284 
285             // Keeps track of last added child, to hook up to next added child.
286             MiniTreeIndividual mtiRightmostChild = null;
287 
288             // For each sibling (including the subject)
289             while (true)
290             {
291                 GDMIndividualRecord irSibling = GetChild(familyParents, siblings, irSubject);
292                 if (irSibling == null) {
293                     break;
294                 }
295 
296                 if (irSibling == irSubject) {
297                     // Add spouses and children of subject, (and subject too, if we need to put wife after them.)
298                     MiniTreeGroup mtgOffspring = null;
299                     bool addedSubject = false;
300                     int nSpouses = 0;
301                     var ecbCrossbar = MiniTreeGroup.ECrossbar.Solid;
302                     var indiFamilies = GMHelper.GetFamilyList(fTree, irSubject);
303 
304                     foreach (GDMFamilyRecord famRec in indiFamilies) {
305                         GDMIndividualRecord irSpouse = fTree.GetSpouseBy(famRec, irSubject);
306 
307                         if (famRec.Husband.XRef != irSubject.XRef) {
308                             mtiRightmostSibling = AddToGroup(irSpouse, mtgSiblings);
309                             // Subject is female so all but last husband have dotted bars
310                             ecbCrossbar = MiniTreeGroup.ECrossbar.DottedLeft;
311                         } else if (Exists(irSubject) && !addedSubject) {
312                             // Subject is male, so need to put them in now, before their children.
313                             // (Otherwise they get added as a regular sibling later)
314                             var boxtext = new CBoxText(irSubject);
315                             mtiRightmostSibling = mtgSiblings.AddIndividual(irSubject, boxtext.FirstName, boxtext.Surname, boxtext.Date, false, familyParents != null, true, boxtext.Concealed, false);
316 
317                             // To stop subject being added as regular sibling.
318                             addedSubject = true;
319                         }
320 
321                         int grandChildren = 0;
322                         GDMIndividualRecord irGrandChild = null;
323 
324                         // If we have already added an offspring box (from previous marriage) need connect this box to it as its right box.
325                         if (mtgOffspring != null) {
326                             mtgOffspring.RightBox = mtiRightmostSibling;
327                         }
328 
329                         // Create a box for the offspring of this marriage
330                         mtgOffspring = new MiniTreeGroup();
331 
332                         // Set crossbar that joins subject to spouse according to whether this is subject's first spouse.
333                         mtgOffspring.fCrossbar = ecbCrossbar;
334 
335                         // Add children by this spouse
336                         MiniTreeIndividual mtiChild = null;
337                         while ((irGrandChild = GetChild(famRec, grandChildren, null)) != null) {
338                             if (Exists(irGrandChild)) {
339                                 var boxtext = new CBoxText(irGrandChild);
340                                 mtiChild = mtgOffspring.AddIndividual(irGrandChild, boxtext.FirstName, boxtext.Surname, boxtext.Date, true, true, false, boxtext.Concealed, false);
341 
342                                 // Hook this up to any children by previous spouses.
343                                 if (grandChildren == 0 && mtiRightmostChild != null) {
344                                     mtiRightmostChild.RightObjectAlien = mtiChild;
345                                     mtiChild.LeftObjectAlien = mtiRightmostChild;
346                                 }
347                             }
348                             grandChildren++;
349                         }
350 
351                         // If we added anything, record it as the right-most child ready to hook to children by next spouse.
352                         if (mtiChild != null) {
353                             mtiRightmostChild = mtiChild;
354                         }
355 
356                         // Add the subjects children to the siblings group
357                         mtgSiblings.AddGroup(mtgOffspring);
358 
359                         // Hook the offspring group to the previous sibling
360                         if (mtgOffspring != null) {
361                             mtgOffspring.LeftBox = mtiRightmostSibling;
362                         }
363 
364                         // If subject is husband then we need to add their wife now.
365                         if (famRec.Husband.XRef == irSubject.XRef) {
366                             ecbCrossbar = MiniTreeGroup.ECrossbar.DottedRight;
367 
368                             // Hook up to previous rightmost sibling and set this as new rightmost sibling.
369                             mtiRightmostSibling = AddToGroup(irSpouse, mtgSiblings);
370 
371                             // Hook the wife up as box on right of offspring box.
372                             if (mtgOffspring != null) {
373                                 mtgOffspring.RightBox = mtiRightmostSibling;
374                             }
375                         }
376 
377                         nSpouses++;
378                     }
379 
380                     if (!addedSubject) {
381                         var boxtext = new CBoxText(irSubject);
382                         MiniTreeIndividual mtiWife = mtgSiblings.AddIndividual(irSubject, boxtext.FirstName, boxtext.Surname, boxtext.Date, false, familyParents != null, true, boxtext.Concealed, false);
383 
384                         if (mtgOffspring != null) {
385                             mtgOffspring.fCrossbar = MiniTreeGroup.ECrossbar.Solid;
386                             mtgOffspring.RightBox = mtiWife;
387                         }
388                     }
389                 } else if (Exists(irSibling)) {
390                     // A sibling (not the subject).
391                     var boxtext = new CBoxText(irSibling);
392                     mtgSiblings.AddIndividual(irSibling, boxtext.FirstName, boxtext.Surname, boxtext.Date, true, familyParents != null, true, boxtext.Concealed, false);
393                 }
394 
395                 siblings++;
396             }
397 
398             // Add siblings group after subject's father
399             mtgParents.AddGroup(mtgSiblings);
400 
401             // Hook up to subject's father
402             mtgSiblings.LeftBox = mtiFather;
403 
404             // Add subject's mother
405             if (familyParents != null) {
406                 MiniTreeIndividual mtiMother = AddToGroup(wife, mtgParents);
407                 mtgSiblings.RightBox = mtiMother;
408             }
409 
410             // Return the parents group (which contains the other family groups).
411             return mtgParents;
412         }
413 
414         // Gets the n'th child in the fr, or returns the default individual if first child requested and no fr.
GetChild(GDMFamilyRecord famRec, int childIndex, GDMIndividualRecord irDefault)415         private GDMIndividualRecord GetChild(GDMFamilyRecord famRec, int childIndex, GDMIndividualRecord irDefault)
416         {
417             GDMIndividualRecord irChild = null;
418             if (famRec != null && childIndex < famRec.Children.Count) {
419                 // The ordering of children in the tree can be selected to be the same as it is in the GEDCOM file. This
420                 // is because the file should be ordered as the user chose to order the fr when entering the data in
421                 // their fr history app, regardless of actual birth dates.
422                 if (GMConfig.Instance.KeepSiblingOrder) {
423                     irChild = fTree.GetPtrValue(famRec.Children[childIndex]);
424                 } else {
425                     irChild = fTree.GetPtrValue(famRec.Children[childIndex]);
426                 }
427             } else {
428                 // Return the default individual as first and only child of fr.
429                 if (childIndex == 0) {
430                     irChild = irDefault;
431                 }
432             }
433             return irChild;
434         }
435 
436         // Add a box for the individual to the specified group.
AddToGroup(GDMIndividualRecord ir, MiniTreeGroup mtg)437         private static MiniTreeIndividual AddToGroup(GDMIndividualRecord ir, MiniTreeGroup mtg)
438         {
439             MiniTreeIndividual mti;
440             if (Exists(ir)) {
441                 CBoxText boxtext = new CBoxText(ir);
442                 mti = mtg.AddIndividual(ir, boxtext.FirstName, boxtext.Surname, boxtext.Date, true, false, false, boxtext.Concealed, true);
443             } else {
444                 mti = mtg.AddIndividual(null, "", GMConfig.Instance.UnknownName, " ", false, false, false, false, true);
445             }
446             return mti;
447         }
448 
449         // Returns true if the supplied record is valid for inclusion in the tree
Exists(GDMIndividualRecord ir)450         private static bool Exists(GDMIndividualRecord ir)
451         {
452             return (ir != null && GMHelper.GetVisibility(ir));
453         }
454     }
455 }
456