-
Notifications
You must be signed in to change notification settings - Fork 3
/
fs-pedigree-ancestor-group.html
572 lines (514 loc) · 19.3 KB
/
fs-pedigree-ancestor-group.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
<link rel="import" href="../polymer/polymer-element.html">
<link rel="import" href="../fs-request/fs-request.html">
<link rel="import" href="../fs-api-aware/fs-api-aware.html">
<link rel="import" href="fs-pedigree-couple.html">
<!--
@customElement
@polymer
@demo demo/index.html
-->
<dom-module id="fs-pedigree-ancestor-group">
<template>
<style>
:host {
display: block;
position: absolute;
}
svg {
height: 100%;
width: 100%;
}
.line {
display: none;
stroke: #333;
stroke-width: 1px;
fill: none;
}
</style>
<svg id="svg">
<g>
<path id="line1" class="line"></path>
<path id="line2" class="line"></path>
<path id="line3" class="line"></path>
<path id="line4" class="line"></path>
<path id="line5" class="line"></path>
<path id="line6" class="line"></path>
<path id="line7" class="line"></path>
</g>
</svg>
<div id="couples"></div>
<fs-request
id="request"
loading="{{loading}}"
require-authentication
client-name="[[clientName]]"
url="{{_computeRequestUrl(personId, spouseId)}}"
on-response="_handleResponse"></fs-request>
</template>
<script>
(function(){
const COUPLE_WIDTH = 250;
const COUPLE_HEIGHT = 115;
const COUPLE_SPACING = 30;
const COUPLE_WIDTH_HALF = COUPLE_WIDTH / 2;
const COUPLE_HEIGHT_HALF = COUPLE_HEIGHT / 2;
const COUPLE_HEIGHT_AND_SPACING = COUPLE_HEIGHT + COUPLE_SPACING;
const COUPLE_HEIGHT_AND_SPACING_HALF = COUPLE_HEIGHT_AND_SPACING / 2;
const COUPLE_COL2_LEFT = COUPLE_WIDTH * .66;
const COUPLE_COL3_LEFT = COUPLE_COL2_LEFT + COUPLE_WIDTH + COUPLE_SPACING;
const COUPLE_ROOT_TOP = (2 * COUPLE_HEIGHT_AND_SPACING) - COUPLE_HEIGHT_AND_SPACING_HALF;
const COUPLE_2_TOP = COUPLE_HEIGHT_AND_SPACING - COUPLE_HEIGHT_AND_SPACING_HALF;
const COUPLE_3_TOP = (3 * COUPLE_HEIGHT_AND_SPACING) - COUPLE_HEIGHT_AND_SPACING_HALF;
const COUPLE_4_TOP = 0;
const COUPLE_5_TOP = COUPLE_HEIGHT_AND_SPACING;
const COUPLE_6_TOP = 2 * COUPLE_HEIGHT_AND_SPACING;
const COUPLE_7_TOP = 3 * COUPLE_HEIGHT_AND_SPACING;
const LINE_COL2_LEFT = COUPLE_WIDTH_HALF;
const LINE_COL2_HEIGHT = COUPLE_HEIGHT + COUPLE_SPACING;
const LINE_COL2_WIDTH = COUPLE_COL2_LEFT - LINE_COL2_LEFT;
const LINE_COL3_LEFT = COUPLE_COL2_LEFT + COUPLE_WIDTH_HALF;
const LINE_COL3_HEIGHT = COUPLE_2_TOP - COUPLE_HEIGHT_HALF;
const LINE_COL3_WIDTH = COUPLE_WIDTH_HALF + COUPLE_SPACING;
const TOTAL_WIDTH = COUPLE_COL3_LEFT + COUPLE_WIDTH;
const TOTAL_HEIGHT = (3 * COUPLE_HEIGHT_AND_SPACING) + COUPLE_HEIGHT;
class FsPedigreeAncestorGroup extends FSApiAwareMixin(Polymer.Element) {
static get is() { return 'fs-pedigree-ancestor-group'; }
static get properties() {
return {
/** ID of the root person */
personId: {
type: String
},
/**
* ID of the root person's spouse, if known. When not specified, the
* API will automatically choose a spouse. You may set this property
* to specify which spouse will be loaded.
*/
spouseId: {
type: String,
value: ''
},
/** Whether to show the "+ Add Person" buttons in the pedigree when a person is missing */
addPersons: {
type: Boolean,
value: false
},
/**
* Whether this is the root group. The root group displays the root
* couple and two additional generations. Non-root groups don't display
* the root couple (they're already displayed) and just display the
* two additional generatiosn.
*/
rootGroup: {
type: Boolean,
value: false
},
/** Whether this group of ancestors is loading */
loading: {
type: Boolean,
value: false,
notify: true
},
/** Which level this group represents. */
level: {
type: Number,
value: 1
},
/** Pointer to the next ancestor group */
_nextGroup: Object,
_couples: {
type: Array,
value: function(){
return [
{
couplePosition: 1,
husbandAhnen: 2,
wifeAhnen: 3,
left: 0,
top: COUPLE_ROOT_TOP
},
{
couplePosition: 2,
husbandAhnen: 4,
wifeAhnen: 5,
left: COUPLE_COL2_LEFT,
top: COUPLE_2_TOP
},
{
couplePosition: 3,
husbandAhnen: 6,
wifeAhnen: 7,
left: COUPLE_COL2_LEFT,
top: COUPLE_3_TOP
},
{
couplePosition: 4,
husbandAhnen: 8,
wifeAhnen: 9,
left: COUPLE_COL3_LEFT,
top: COUPLE_4_TOP
},
{
couplePosition: 5,
husbandAhnen: 10,
wifeAhnen: 11,
left: COUPLE_COL3_LEFT,
top: COUPLE_5_TOP
},
{
couplePosition: 6,
husbandAhnen: 12,
wifeAhnen: 13,
left: COUPLE_COL3_LEFT,
top: COUPLE_6_TOP
},
{
couplePosition: 7,
husbandAhnen: 14,
wifeAhnen: 15,
left: COUPLE_COL3_LEFT,
top: COUPLE_7_TOP
}
];
}
}
};
}
ready() {
super.ready();
this.reload();
this.style.width = TOTAL_WIDTH + 'px';
this.style.height = TOTAL_HEIGHT + 'px';
this._drawLines();
}
reload() {
this._clearCouples();
this._hideLines();
this.$.request.generateRequest();
}
/**
* Collapse (remove) this portion of the pedigree.
*/
collapse() {
if(this.nextGroup) {
this.nextGroup.collapse();
}
this.remove();
}
/**
* Position the group to be in line with the given coordinates of the
* couple group that was extended.
*
* @param {String} left Left position of the couple, in pixels
* @param {String} top Top position of the couple, in pixels. The group
* will be vertically centered with this position.
*/
positionWithCouple(left, top) {
const x = parseInt(left, 10);
const y = parseInt(top, 10);
this.style.left = `${x + COUPLE_WIDTH - COUPLE_SPACING}px`;
this.style.top = `${(y + COUPLE_HEIGHT_HALF) - (TOTAL_HEIGHT / 2)}px`;
}
_computeRequestUrl(personId = '', spouseId = '') {
if(!personId) {
personId = spouseId;
spouseId = '';
}
return `/platform/tree/ancestry?person=${personId}&spouse=${spouseId}&generations=2`;
}
_handleResponse(e) {
const response = e.detail.response;
if(response && response.data && response.data.persons) {
// Put persons into a map by ahnen number
const persons = {};
response.data.persons.forEach((person) => {
persons[person.display.ascendancyNumber] = person;
});
// Construct couples
this._couples.forEach((couple) => {
couple.husband = persons[couple.husbandAhnen];
couple.wife = persons[couple.wifeAhnen];
});
this._displayCouples();
}
}
/**
* Create couple elements and add them to the DOM.
*/
_displayCouples() {
this._couples.forEach((coupleData, i) => {
// if(this.addPersons || coupleData.wife || coupleData.husband) {
if(this._displayCouple(coupleData)) {
// Create the couple, position them, and add to the DOM.
// Don't add the 1st couple if we're not a root node.
if(coupleData.couplePosition !== 1 || this.rootGroup) {
const coupleElement = document.createElement('fs-pedigree-couple');
coupleElement.husband = coupleData.husband;
coupleElement.wife = coupleData.wife;
coupleElement.clientName = this.clientName;
coupleElement.addPersons = this.addPersons;
coupleElement.style.left = coupleData.left + 'px';
coupleElement.style.top = coupleData.top + 'px';
coupleElement.style.width = COUPLE_WIDTH + 'px';
coupleElement.style.position = 'absolute';
coupleElement.style.height = COUPLE_HEIGHT + 'px';
this.$.couples.appendChild(coupleElement);
// Listen for couple events
coupleElement.addEventListener('extend-couple', this._extendCouple.bind(this));
coupleElement.addEventListener('collapse-couple', this._collapseCouple.bind(this));
coupleElement.addEventListener('add-husband', (e) => {
this._addPerson(coupleData);
});
coupleElement.addEventListener('add-wife', (e) => {
this._addPerson(coupleData);
});
if(coupleData.couplePosition > 3) {
coupleElement.extend = true;
}
}
// Show the start lines if we're not a root node and show
// other lines if the couple exists.
if((coupleData.couplePosition === 1 && !this.rootGroup) || coupleData.couplePosition > 1) {
this._showLine(coupleData.couplePosition);
}
}
});
}
/**
* Calculate whether a couple group should be displayed.
* They are displayed when either the husband or wife is present
* or if addPersons is true and their child is present.
*/
_displayCouple(couple) {
return couple.wife || couple.husband || (
this.addPersons && this._coupleHasChild(couple.couplePosition)
);
}
/**
* Calculate whether the couple has a child. This is used when calculating
* whether a particular couple group should be shown. When adding persons,
* we want to show empty couples so that parents can be added. This method
* is called on those empty couples so that we can check whether there is
* a child that those parents belong to. Otherwise we don't want ot show
* the couple because we can't add parents to a person that doesn't exist.
*/
_coupleHasChild(parentCoupleNum) {
return !!this._getCouplesChild(parentCoupleNum);
}
/**
* Get a couple's child
*
* @param {Number} parentCoupleNum
*/
_getCouplesChild(parentCoupleNum) {
const childCoupleNum = this._calculateChildCoupleNumber(parentCoupleNum);
// Shouldn't happen
if(!childCoupleNum) {
return;
}
// Even couples are parents of males; odd couples are parents of females.
const childGender = parentCoupleNum % 2 === 0 ? 'male' : 'female';
const childCouple = this._couples.find((c) => c.couplePosition === childCoupleNum);
if(childCouple) {
if(childGender === 'male') {
return childCouple.husband;
}
if(childGender === 'female') {
return childCouple.wife;
}
}
}
/**
* Calculate the child's couple number of another couple
*
* @param {Number} parentCoupleNum
*/
_calculateChildCoupleNumber(parentCoupleNum) {
switch(parentCoupleNum) {
case 1: // This shouldn't be called, but just in case...
return 0;
case 2:
case 3:
return 1;
case 4:
case 5:
return 2;
case 6:
case 7:
return 3;
}
}
/**
* Extend the pedigree from the given couple.
*/
_extendCouple(e) {
this._collapseCouple(e);
const couple = e.target;
const position = this._calculateAbsoluteCouplePosition(e);
const group = document.createElement('fs-pedigree-ancestor-group');
group.personId = e.detail.husbandId;
group.spouseId = e.detail.wifeId;
group.clientName = this.clientName;
group.level = this.level + 1;
group.addPersons = this.addPersons;
this.nextGroup = group;
group.positionWithCouple(position.left, position.top);
group.addEventListener('loading-changed', (e) => {
couple.loading = e.detail.value;
});
// We want the new groups to be layered underneath previous groups
// so we add the new group before the existing group in the DOM.
this.parentElement.prepend(group);
}
/**
* Collapse the pedigree at the given couple.
*/
_collapseCouple(e) {
// Remove higher groups
if(this.nextGroup) {
this.nextGroup.collapse();
this.nextGroup = undefined;
}
// Revert state of other couples
Array.from(this.$.couples.children).forEach((couple) => {
if(couple !== e.target) {
couple.extended = false;
}
});
}
/**
* Couple events contain information about the position of
* the couple element. That position is used to position the new
* ancestor group. However that position is relative to this couple
* group so here we modify the position to be relative to the
* pedigree container by adding this element's position to the
* couple element's position.
*/
_calculateAbsoluteCouplePosition(e) {
const coupleLeft = parseInt(e.detail.left, 10);
const thisLeft = this.style.left ? parseInt(this.style.left, 10) : 0;
const coupleTop = parseInt(e.detail.top, 10);
const thisTop = this.style.top ? parseInt(this.style.top, 10) : 0;
return {
left: `${coupleLeft + thisLeft}px`,
top: `${coupleTop + thisTop}px`
};
}
/**
* Remove persons from the couple config object and
* clear couples from the DOM.
*/
_clearCouples() {
this._couples.forEach((couple) => {
couple.wife = null;
couple.husband = null;
});
this.$.couples.innerHTML = '';
}
/**
* Emit the add-person event
*
* @param {Object} couple
*/
_addPerson(couple) {
const child = this._getCouplesChild(couple.couplePosition);
this.dispatchEvent(new CustomEvent('add-person', {
detail: {
fatherId: couple.husband ? couple.husband.id : undefined,
motherId: couple.wife ? couple.wife.id : undefined,
childId: child ? child.id : undefined
},
bubbles: true,
composed: true
}));
}
/**
* Show a line.
*
* @param {Integer} num Line number
*/
_showLine(num) {
const line = this.$[this._lineId(num)];
if(line) {
line.style.display = 'block';
}
}
/**
* Hide all lines. The default display value is none (via CSS) so here
* we just clear the value we set when showing the line.
*/
_hideLines() {
this.$.svg.querySelectorAll('path').forEach((path) => {
path.style.display = '';
});
}
/**
* Draw lines but don't modify their display. Default display is none.
* We will decide later whether they need to be shown.
*/
_drawLines() {
this._drawStartLine();
// Lines 2 and 3 need to account for the root couple not being drawn
// when this isn't a root group. Therefore instead of stopping at the
// border of the root couple we extend to the middle. When the root couple
// is drawn part of the line will be covered. When the root couple isn't
// drawn then the lines will connect as we want.
this._drawOtherLine(2, LINE_COL2_LEFT, COUPLE_ROOT_TOP + COUPLE_HEIGHT_HALF, -LINE_COL2_HEIGHT, LINE_COL2_WIDTH);
this._drawOtherLine(3, LINE_COL2_LEFT, COUPLE_ROOT_TOP + COUPLE_HEIGHT_HALF, LINE_COL2_HEIGHT, LINE_COL2_WIDTH);
this._drawOtherLine(4, LINE_COL3_LEFT, COUPLE_2_TOP, -LINE_COL3_HEIGHT, LINE_COL3_WIDTH);
this._drawOtherLine(5, LINE_COL3_LEFT, COUPLE_2_TOP + COUPLE_HEIGHT, LINE_COL3_HEIGHT, LINE_COL3_WIDTH);
this._drawOtherLine(6, LINE_COL3_LEFT, COUPLE_3_TOP, -LINE_COL3_HEIGHT, LINE_COL3_WIDTH);
this._drawOtherLine(7, LINE_COL3_LEFT, COUPLE_3_TOP + COUPLE_HEIGHT, LINE_COL3_HEIGHT, LINE_COL3_WIDTH);
}
/**
* Draw the beginning lines for a non-root group. They are the lines that
* stretch out to the left to connect to the couple where the pedigree
* was expanded.
*/
_drawStartLine() {
// We already have lines extending out from couples 2 and 3 so we just
// need to connect them to the couple group on the left that we
// extended from with a horizontal line at the middle vertical position.
this._drawLine(1, `M 0 ${TOTAL_HEIGHT / 2} h ${COUPLE_WIDTH_HALF}`);
}
/**
* Draw a path that connects two couples. Lines are drawn starting with a
* couple group to the left. The line proceeds vertically up or down then
* turns to the right and connects horizontally to the left side of the
* final couple group.
*
* @param {Integer} num Line number. Line numbers are associated with the
* couple group on the right hat the path connects to horizontally.
* @param {Integer} startX The starting x coordinate for the vertical
* portion of the line.
* @param {Integer} startY The starting y coordinate for the vertical
* portion of the line.
* @param {Integer} vertical The length of the vertical portion of the line.
* @param {Integer} horizontal The length of the horizontal portion of the line.
*/
_drawOtherLine(num, startX, startY, vertical, horizontal) {
this._drawLine(num, `M ${startX} ${startY} v ${vertical} h ${horizontal}`);
}
/**
* Create a path element and contruct it's path
*
* @param {Integer} num Line number.
* @param {String} path SVG path description
*/
_drawLine(num, path) {
const line = this.$[this._lineId(num)];
if(line) {
line.setAttribute('d', path);
}
}
/**
* Given a line number, return it's id
*/
_lineId(num) {
return `line${num}`;
}
}
customElements.define(FsPedigreeAncestorGroup.is, FsPedigreeAncestorGroup);
}());
</script>
</dom-module>