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