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 GDModel; 22 23 namespace GEDmill.MiniTree 24 { 25 /// <summary> 26 /// Represents a group of individuals in the tree, e.g. a group of siblings, that need to be kept together 27 /// </summary> 28 public class MiniTreeGroup : MiniTreeObject 29 { 30 // Type of gedcomLine joining this box horizontally to indicate marriage or other parenting relationship. 31 public enum ECrossbar 32 { 33 Solid = 0, 34 DottedLeft = 1, 35 DottedRight = 2 36 } 37 38 // The CMiniTreeIndividual boxes and groups that make up this group. 39 private List<MiniTreeObject> fMembers; 40 41 // Width of the gedcomLine joining this group to group above. 42 private const float TEE_WIDTH = 0.0f; 43 44 // Height of the gedcomLine joining this group to group above. 45 private const float TEE_HEIGHT = 16.0f; 46 47 // The screen size of this group 48 private SizeF fSize; 49 50 // The group that this group belongs to. 51 private MiniTreeGroup fParent; 52 53 // The number of individuals in this group (as opposed to groups) 54 private uint fIndividuals; 55 56 // Number of individuals in this group with lines up to the generation above. 57 private uint fStalkedIndividuals; 58 59 // The left-most box. 60 private MiniTreeIndividual fBoxLeft; 61 62 // The right-most box. 63 private MiniTreeIndividual fBoxRight; 64 65 // Reference to the last box to be added to this group 66 private MiniTreeObject fLastAddedObject; 67 68 // Type of gedcomLine joining this box horizontally to indicate marriage or other parenting relationship. 69 public ECrossbar fCrossbar; 70 71 72 // Returns the left-most box. (Used when drawing horizontal gedcomLine from which children hang.) 73 public MiniTreeIndividual LeftBox 74 { 75 get { return fBoxLeft; } 76 set { fBoxLeft = value; } 77 } 78 79 // Returns the right-most box. (Used when drawing horizontal gedcomLine from which children hang.) 80 public MiniTreeIndividual RightBox 81 { 82 get { return fBoxRight; } 83 set { fBoxRight = value; } 84 } 85 86 MiniTreeGroup()87 public MiniTreeGroup() 88 { 89 fMembers = null; 90 fSize = new SizeF(0.0f, 0.0f); 91 fParent = null; 92 fIndividuals = 0; 93 fStalkedIndividuals = 0; 94 fBoxLeft = null; 95 fBoxRight = null; 96 fLastAddedObject = null; 97 fCrossbar = ECrossbar.Solid; 98 } 99 100 // Creates a CMiniTreeIndividual for the individual specified and adds it to the group. 101 // Informs neighbouring boxes about this box. 102 // bCreateLink decides whether to make this box a clickable link in the HTML. AddIndividual(GDMIndividualRecord ir, string firstnames, string surname, string date, bool createLink, bool createStalk, bool highlight, bool concealed, bool shade)103 public MiniTreeIndividual AddIndividual(GDMIndividualRecord ir, string firstnames, string surname, 104 string date, bool createLink, bool createStalk, bool highlight, 105 bool concealed, bool shade) 106 { 107 MiniTreeIndividual mti = new MiniTreeIndividual(ir, firstnames, surname, date, createLink, createStalk, 108 highlight, concealed, shade, GMConfig.Instance.ConserveTreeWidth); 109 110 if (fMembers == null) { 111 fMembers = new List<MiniTreeObject>(); 112 } 113 fMembers.Add(mti); 114 115 fIndividuals++; 116 117 if (createStalk) { 118 fStalkedIndividuals++; 119 } 120 121 mti.LeftObject = fLastAddedObject; 122 123 if (fLastAddedObject != null) { 124 fLastAddedObject.RightObject = mti; 125 } 126 127 fLastAddedObject = mti; 128 129 return mti; 130 } 131 132 // Adds a CMiniTreeGroup to this group. 133 // Informs neighbouring boxes about the group. AddGroup(MiniTreeGroup mtg)134 public void AddGroup(MiniTreeGroup mtg) 135 { 136 if (mtg != null) { 137 if (fMembers == null) { 138 fMembers = new List<MiniTreeObject>(); 139 } 140 fMembers.Add(mtg); 141 142 mtg.fParent = this; 143 144 mtg.LeftObject = fLastAddedObject; 145 146 if (fLastAddedObject != null) { 147 fLastAddedObject.RightObject = mtg; 148 } 149 150 fLastAddedObject = mtg; 151 } 152 } 153 154 // Calculates the size required by this group. Initialises the class fields 155 // that contain size information. Returns the overall group size. CalculateSize(Graphics g, Font f)156 public override SizeF CalculateSize(Graphics g, Font f) 157 { 158 fSize.Width = 0.0f; 159 fSize.Height = 0.0f; 160 161 if (fMembers == null) { 162 // Empty group 163 return fSize; 164 } 165 166 foreach (var obj in fMembers) { 167 SizeF size; 168 if (obj is MiniTreeIndividual) { 169 size = ((MiniTreeIndividual)obj).CalculateSize(g, f); 170 } else if (obj is MiniTreeGroup) { 171 // Let group calculate its size for later 172 ((MiniTreeGroup)obj).CalculateSize(g, f); 173 174 // Size here is only size of tee 175 size = new SizeF(TEE_WIDTH, TEE_HEIGHT); 176 } else { 177 size = new SizeF(0f, 0f); 178 } 179 180 fSize.Width += size.Width; 181 if (size.Height > fSize.Height) { 182 fSize.Height = size.Height; 183 } 184 } 185 186 if (fIndividuals == 0) { 187 // Don't include tee size if no individuals 188 fSize.Width = 0f; 189 fSize.Height = 0f; 190 } 191 192 return fSize; 193 } 194 195 // Draws the group to the graphics instance. DrawBitmap(Paintbox paintbox, Graphics g, List<MiniTreeMap> map)196 public override void DrawBitmap(Paintbox paintbox, Graphics g, List<MiniTreeMap> map) 197 { 198 if (fMembers == null) { 199 // Empty group 200 return; 201 } 202 203 foreach (var obj in fMembers) { 204 if (obj is MiniTreeGroup) { 205 var mtg = (MiniTreeGroup)obj; 206 if (mtg.fBoxLeft != null && mtg.fBoxRight != null) { 207 // Draw crossbar 208 float crossbarLeft = mtg.fBoxLeft.TeeRight; 209 float crossbarRight = mtg.fBoxRight.TeeLeft; 210 float crossbarLeftGap = mtg.fBoxLeft.Right; 211 float crossbarRightGap = mtg.fBoxRight.Left; 212 float crossbarY = (mtg.fBoxLeft.TeeCentreVert + mtg.fBoxRight.TeeCentreVert) / 2f; 213 switch (mtg.fCrossbar) { 214 case ECrossbar.Solid: 215 g.DrawLine(paintbox.PenConnector, crossbarLeft, crossbarY, crossbarRight, crossbarY); 216 break; 217 218 case ECrossbar.DottedLeft: 219 g.DrawLine(paintbox.PenConnectorDotted, crossbarLeft, crossbarY, crossbarRightGap, crossbarY); 220 break; 221 222 case ECrossbar.DottedRight: 223 g.DrawLine(paintbox.PenConnectorDotted, crossbarLeftGap, crossbarY, crossbarRight, crossbarY); 224 break; 225 } 226 227 if (mtg.fStalkedIndividuals > 0) { 228 // Draw down to individuals 229 // Use y coord of first individual, assuming all are at the same y coord 230 float individualY = 0f; 231 bool haveIndividuals = false; 232 foreach (MiniTreeObject groupObj in mtg.fMembers) { 233 if (groupObj is MiniTreeIndividual) { 234 individualY = ((MiniTreeIndividual)groupObj).Top; 235 haveIndividuals = true; 236 break; 237 } 238 } 239 float crossbarCentre = (crossbarLeft + crossbarRight) / 2f; 240 if (haveIndividuals) { 241 g.DrawLine(paintbox.PenConnector, crossbarCentre, crossbarY, crossbarCentre, individualY); 242 243 // Connect individuals 244 SizeF stalkMinMax = mtg.StalkMinMax; 245 246 // Width irrelevant, using SizeF simply as a way to pass 2 floats: 247 float stalkMin = stalkMinMax.Width; 248 249 // Height irrelevant, using SizeF simply as a way to pass 2 floats 250 float stalkMax = stalkMinMax.Height; 251 252 if (crossbarCentre < stalkMin) { 253 stalkMin = crossbarCentre; 254 } else if (crossbarCentre > stalkMax) { 255 stalkMax = crossbarCentre; 256 } 257 g.DrawLine(paintbox.PenConnector, stalkMin, individualY, stalkMax, individualY); 258 } 259 } 260 } 261 262 mtg.DrawBitmap(paintbox, g, map); 263 } else if (obj is MiniTreeIndividual) { 264 // Draw individual box 265 ((MiniTreeIndividual)obj).DrawBitmap(paintbox, g, map); 266 } 267 } 268 } 269 270 // Returns the size occupied by all the boxes in this group and its sub groups. 271 // Caller must ensure members != null otherwise they will get returned an invalid rectangle. GetExtent()272 public RectangleF GetExtent() 273 { 274 float top = 0f, right = 0f, bottom = 0f, left = 0f; 275 if (fMembers != null) { 276 bool first = true; 277 foreach (MiniTreeObject obj in fMembers) { 278 if (obj is MiniTreeIndividual) { 279 var mtIndi = (MiniTreeIndividual)obj; 280 float individualTop = mtIndi.Top; 281 float individualBottom = mtIndi.Bottom; 282 float individualLeft = mtIndi.Left; 283 float individualRight = mtIndi.Right; 284 if (first || individualTop < top) { 285 top = individualTop; 286 } 287 if (first || individualBottom > bottom) { 288 bottom = individualBottom; 289 } 290 if (first || individualLeft < left) { 291 left = individualLeft; 292 } 293 if (first || individualRight > right) { 294 right = individualRight; 295 } 296 first = false; 297 } else if (obj is MiniTreeGroup) { 298 if (((MiniTreeGroup)obj).fMembers != null) { 299 RectangleF rectSubGroup = ((MiniTreeGroup)obj).GetExtent(); 300 301 if (first || rectSubGroup.Top < top) { 302 top = rectSubGroup.Top; 303 } 304 if (first || rectSubGroup.Bottom > bottom) { 305 bottom = rectSubGroup.Bottom; 306 } 307 if (first || rectSubGroup.Left < left) { 308 left = rectSubGroup.Left; 309 } 310 if (first || rectSubGroup.Right > right) { 311 right = rectSubGroup.Right; 312 } 313 first = false; 314 } 315 } 316 } 317 } 318 return new RectangleF(left, top, right - left, bottom - top); 319 } 320 321 // Moves the position of the boxes in this group and its sub groups by an absolute amount. Translate(float deltaX, float deltaY)322 public override void Translate(float deltaX, float deltaY) 323 { 324 if (fMembers != null) { 325 foreach (MiniTreeObject obj in fMembers) { 326 obj.Translate(deltaX, deltaY); 327 } 328 } 329 } 330 331 // Calculates how to lay out this group to "look right" 332 // Must have called CalculateSize on all groups first. CalculateLayout(float x, float y)333 public override SizeF CalculateLayout(float x, float y) 334 { 335 SizeF sizeMax = new SizeF(0f, 0f); 336 float startX = x; 337 float height = 0f; 338 float heightChild = 0f; 339 340 if (fMembers == null) { 341 // Empty group 342 return new SizeF(0f, 0f); 343 } 344 345 foreach (var obj in fMembers) { 346 if (obj is MiniTreeGroup) { 347 SizeF size = ((MiniTreeGroup)obj).CalculateLayout(x, y + fSize.Height); 348 x += size.Width; 349 if (heightChild < size.Height) { 350 heightChild = size.Height; 351 } 352 } else if (obj is MiniTreeIndividual) { 353 SizeF size = ((MiniTreeIndividual)obj).CalculateLayout(x, y); 354 x += size.Width; 355 if (height < size.Height) { 356 height = size.Height; 357 } 358 } 359 } 360 361 sizeMax.Width = x - startX; 362 sizeMax.Height = height + heightChild; 363 return sizeMax; 364 } 365 366 // Improve the layout by moving boxes closer to each other. Compress()367 public void Compress() 368 { 369 if (fMembers == null) { 370 // Empty group 371 return; 372 } 373 374 foreach (MiniTreeObject obj in fMembers) { 375 var mtGroup = obj as MiniTreeGroup; 376 if (mtGroup != null) { 377 // Propagate the compression. 378 mtGroup.Compress(); 379 } 380 } 381 382 // Some groups are containers for other groups only (where an individuals 383 // frParents are not known and there is no fr structure for the individual) 384 if (fStalkedIndividuals > 0) { 385 SizeF stalkMinMax = StalkMinMax; 386 387 // Width irrelevant, using SizeF simply as a way to pass 2 floats 388 float stalkMin = stalkMinMax.Width; 389 390 // Height irrelevant, using SizeF simply as a way to pass 2 floats 391 float stalkMax = stalkMinMax.Height; 392 393 // Pull both halves towards centre 394 float centre = (stalkMax + stalkMin) / 2f; 395 396 // The following creates 'mooted' coordinates: 397 398 // Pull as much as allowed 399 PullLeftStuffRight(centre); 400 401 // Pull as much as allowed 402 PullRightStuffLeft(centre); 403 } 404 } 405 406 // Shifts this object and all objects to its left, until one can't move. PullLeft(float amount)407 public override float PullLeft(float amount) 408 { 409 // Did this couple have children? 410 if (fMembers != null) { 411 // Shift the underhanging group members 412 // Find the rightmost underhanging member and shift it left. 413 // That will interact with other members to find max possible shift amount. 414 MiniTreeIndividual mtiRightmost = null; 415 float max = 0; 416 bool first = true; 417 foreach (MiniTreeObject obj in fMembers) { 418 var mtIndi = obj as MiniTreeIndividual; 419 if (mtIndi != null) { 420 if (first || mtIndi.Left > max) { 421 max = mtIndi.Left; 422 mtiRightmost = mtIndi; 423 } 424 first = false; 425 } 426 } 427 if (mtiRightmost != null) { 428 amount = mtiRightmost.PushLeft(amount); 429 } 430 } 431 432 // Now shift right object left. 433 MiniTreeObject mtoRight = RightObject; 434 if (mtoRight != null) { 435 amount = mtoRight.PullLeft(amount); 436 } 437 438 return amount; 439 } 440 441 // Shifts this object and all objects to its right, until one can't move. PullRight(float amount)442 public override float PullRight(float amount) 443 { 444 if (fMembers != null) { 445 // This couple had children 446 447 // Shift the underhanging group members. 448 // Find the leftmost underhanging member and shift it right. 449 // That will interact with other members to find max possible shift amount. 450 MiniTreeIndividual mtiLeftmost = null; 451 float min = 0; 452 bool first = true; 453 foreach (MiniTreeObject obj in fMembers) { 454 var mtIndi = obj as MiniTreeIndividual; 455 if (mtIndi != null) { 456 if (first || mtIndi.Right < min) { 457 min = mtIndi.Right; 458 mtiLeftmost = mtIndi; 459 } 460 first = false; 461 } 462 } 463 if (mtiLeftmost != null) { 464 amount = mtiLeftmost.PushRight(amount); 465 } 466 } 467 468 // Now shift left object right. 469 MiniTreeObject mtoLeft = LeftObject; 470 if (mtoLeft != null) { 471 amount = mtoLeft.PullRight(amount); 472 } 473 474 return amount; 475 } 476 477 // Pushes this object left and all objects to its left left, until one can't move. PushLeft(float amount)478 public override float PushLeft(float amount) 479 { 480 // TODO: Not yet implemented - compression won't be optimal. 481 return amount; 482 } 483 484 // Pushes this object right and all objects to its right right, until one can't move. PushRight(float amount)485 public override float PushRight(float amount) 486 { 487 // TODO: Not yet implemented - compression won't be optimal. 488 return amount; 489 } 490 491 492 // Returns minimum and maximum x coordinate for an upwards gedcomLine from this group's crossbar. 493 // Using SizeF as a way to pass 2 floats. 494 // Caller should check m_nIndividuals first to make sure this property is valid. 495 private SizeF StalkMinMax 496 { 497 get { 498 float min = 0f; 499 float max = 0f; 500 bool first = true; 501 foreach (MiniTreeObject obj in fMembers) { 502 var mtIndi = obj as MiniTreeIndividual; 503 if (mtIndi != null && mtIndi.HasStalk) { 504 float fStalk = mtIndi.Stalk; 505 if (first || fStalk < min) { 506 min = fStalk; 507 } 508 if (first || fStalk > max) { 509 max = fStalk; 510 } 511 first = false; 512 } 513 } 514 515 return new SizeF(min, max); 516 } 517 } 518 519 // Pulls left box as close as can be, to compress group. 520 // Gets left box to pull its left box/group in turn. 521 // Boxes are free to move. Moving a group will pull its members. 522 // They must not collide with other boxes. PullLeftStuffRight(float centre)523 private void PullLeftStuffRight(float centre) 524 { 525 var mtoLeft = LeftObject as MiniTreeIndividual; 526 if (mtoLeft != null) { 527 mtoLeft.PullRight(centre - mtoLeft.Right); 528 } 529 } 530 531 // Pulls right box as close as can be, to compress group. PullRightStuffLeft(float centre)532 private void PullRightStuffLeft(float centre) 533 { 534 var mtoRight = RightObject as MiniTreeIndividual; 535 if (mtoRight != null) { 536 mtoRight.PullLeft(mtoRight.Left - centre); 537 } 538 } 539 } 540 } 541