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