Esempio n. 1
0
    /**
     * Render the Ajax response for the sortable table of Sosa family
     * @param AjaxController $controller
     */
    protected function renderFamSosaListIndi(AjaxController $controller)
    {
        global $WT_TREE;
        $listFamSosa = $this->sosa_provider->getFamilySosaListAtGeneration($this->generation);
        $this->view_bag->set('has_sosa', false);
        if (count($listFamSosa) > 0) {
            $this->view_bag->set('has_sosa', true);
            $table_id = 'table-sosa-fam-' . Uuid::uuid4();
            $this->view_bag->set('table_id', $table_id);
            $controller->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)->addInlineJavascript('
                jQuery.fn.dataTableExt.oSort["unicode-asc"  ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
				jQuery.fn.dataTableExt.oSort["unicode-desc" ]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
				jQuery.fn.dataTableExt.oSort["num-html-asc" ]=function(a,b) {a=parseFloat(a.replace(/<[^<]*>/, "")); b=parseFloat(b.replace(/<[^<]*>/, "")); return (a<b) ? -1 : (a>b ? 1 : 0);};
				jQuery.fn.dataTableExt.oSort["num-html-desc"]=function(a,b) {a=parseFloat(a.replace(/<[^<]*>/, "")); b=parseFloat(b.replace(/<[^<]*>/, "")); return (a>b) ? -1 : (a<b ? 1 : 0);};
        
                jQuery("#' . $table_id . '").dataTable( {
					dom: \'<"H"<"filtersH_' . $table_id . '"><"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
                    ' . I18N::datatablesI18N(array(16, 32, 64, 128, -1)) . ',
					jQueryUI: true,
					autoWidth: false,
					processing: true,
					retrieve: true,
					columns: [
						/* 0-Sosa */  	   { dataSort: 1, class: "center"},
		                /* 1-SOSA */ 	   { type: "num", visible: false },
						/* 2-Husb Givn */  { dataSort: 4},
						/* 3-Husb Surn */  { dataSort: 5},
						/* 4-GIVN,SURN */  { type: "unicode", visible: false},
						/* 5-SURN,GIVN */  { type: "unicode", visible: false},
						/* 6-Husb Age  */  { dataSort: 7, class: "center"},
						/* 7-AGE       */  { type: "num", visible: false},
						/* 8-Wife Givn */  { dataSort: 10},
						/* 9-Wife Surn */  { dataSort: 11},
						/* 10-GIVN,SURN */ { type: "unicode", visible: false},
						/* 11-SURN,GIVN */ { type: "unicode", visible: false},
						/* 12-Wife Age  */ { dataSort: 13, class: "center"},
						/* 13-AGE       */ { type: "num", visible: false},
						/* 14-Marr Date */ { dataSort: 15, class: "center"},
						/* 15-MARR:DATE */ { visible: false},
						/* 16-Marr Plac */ { type: "unicode", class: "center"},
						/* 17-Marr Sour */ { dataSort : 18, class: "center", visible: ' . (ModuleManager::getInstance()->isOperational(Constants::MODULE_MAJ_ISSOURCED_NAME) ? 'true' : 'false') . ' },
						/* 18-Sort Sour */ { visible: false},
						/* 19-Children  */ { dataSort: 20, class: "center"},
						/* 20-NCHI      */ { type: "num", visible: false},
						/* 21-MARR      */ { visible: false},
						/* 22-DEAT      */ { visible: false},
						/* 23-TREE      */ { visible: false}
					],
					sorting: [[0, "asc"]],
					displayLength: 16,
					pagingType: "full_numbers"
			   });
					
				jQuery("#' . $table_id . '")
				/* Hide/show parents */
				.on("click", ".btn-toggle-parents", function() {
					jQuery(this).toggleClass("ui-state-active");
					jQuery(".parents", jQuery(this).closest("table").DataTable().rows().nodes()).slideToggle();
				})
				/* Hide/show statistics */
				.on("click",  ".btn-toggle-statistics", function() {
					jQuery(this).toggleClass("ui-state-active");
					jQuery("#fam_list_table-charts_' . $table_id . '").slideToggle();
				})
				/* Filter buttons in table header */
				.on("click", "button[data-filter-column]", function() {
					var btn = $(this);
					// De-activate the other buttons in this button group
					btn.siblings().removeClass("ui-state-active");
					// Apply (or clear) this filter
					var col = jQuery("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
					if (btn.hasClass("ui-state-active")) {
						btn.removeClass("ui-state-active");
						col.search("").draw();
					} else {
						btn.addClass("ui-state-active");
						col.search(btn.data("filter-value")).draw();
					}
				});					
				
				jQuery("#sosa-fam-list").css("visibility", "visible");
				
				jQuery("#btn-toggle-statistics-' . $table_id . '").click();
           ');
            $stats = new Stats($WT_TREE);
            $max_age = max($stats->oldestMarriageMaleAge(), $stats->oldestMarriageFemaleAge()) + 1;
            //-- init chart data
            $marr_by_age = array();
            for ($age = 0; $age <= $max_age; $age++) {
                $marr_by_age[$age] = '';
            }
            $birt_by_decade = array();
            $marr_by_decade = array();
            for ($year = 1550; $year < 2030; $year += 10) {
                $birt_by_decade[$year] = '';
                $marr_by_decade[$year] = '';
            }
            foreach ($listFamSosa as $sosa => $fid) {
                $sfamily = Family::getInstance($fid, $WT_TREE);
                if (!$sfamily || !$sfamily->canShow()) {
                    unset($sfamily[$sosa]);
                    continue;
                }
                $mdate = $sfamily->getMarriageDate();
                if (($husb = $sfamily->getHusband()) && ($hdate = $husb->getBirthDate()) && $hdate->isOK() && $mdate->isOK()) {
                    if (FunctionsPrint::isDateWithinChartsRange($hdate)) {
                        $birt_by_decade[(int) ($hdate->gregorianYear() / 10) * 10] .= $husb->getSex();
                    }
                    $hage = Date::getAge($hdate, $mdate, 0);
                    if ($hage >= 0 && $hage <= $max_age) {
                        $marr_by_age[$hage] .= $husb->getSex();
                    }
                }
                if (($wife = $sfamily->getWife()) && ($wdate = $wife->getBirthDate()) && $wdate->isOK() && $mdate->isOK()) {
                    if (FunctionsPrint::isDateWithinChartsRange($wdate)) {
                        $birt_by_decade[(int) ($wdate->gregorianYear() / 10) * 10] .= $wife->getSex();
                    }
                    $wage = Date::getAge($wdate, $mdate, 0);
                    if ($wage >= 0 && $wage <= $max_age) {
                        $marr_by_age[$wage] .= $wife->getSex();
                    }
                }
                if ($mdate->isOK() && FunctionsPrint::isDateWithinChartsRange($mdate) && $husb && $wife) {
                    $marr_by_decade[(int) ($mdate->gregorianYear() / 10) * 10] .= $husb->getSex() . $wife->getSex();
                }
                $listFamSosa[$sosa] = $sfamily;
            }
            $this->view_bag->set('sosa_list', $listFamSosa);
            $this->view_bag->set('chart_births', FunctionsPrintLists::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')));
            $this->view_bag->set('chart_marriages', FunctionsPrintLists::chartByDecade($marr_by_decade, I18N::translate('Decade of marriage')));
            $this->view_bag->set('chart_ages', FunctionsPrintLists::chartByAge($marr_by_age, I18N::translate('Age in year of marriage')));
        }
        ViewFactory::make('SosaListFam', $this, $controller, $this->view_bag)->render();
    }
Esempio n. 2
0
				<td class="facts_value">', $stats->minAgeOfMarriageFamily(), '</td>
			</tr>
		</table>
		<br>
		<b>', I18N::translate('Age in year of marriage'), '</b>
		<table class="facts_table">
			<tr>
				<td class="facts_label">', I18N::translate('Youngest male'), ' - ', $stats->youngestMarriageMaleAge(true), '</td>
				<td class="facts_label">', I18N::translate('Youngest female'), ' - ', $stats->youngestMarriageFemaleAge(true), '</td>
			</tr>
			<tr>
				<td class="facts_value">', $stats->youngestMarriageMale(), '</td>
				<td class="facts_value">', $stats->youngestMarriageFemale(), '</td>
			</tr>
			<tr>
				<td class="facts_label">', I18N::translate('Oldest male'), ' - ', $stats->oldestMarriageMaleAge(true), '</td>
				<td class="facts_label">', I18N::translate('Oldest female'), ' - ', $stats->oldestMarriageFemaleAge(true), '</td>
			</tr>
			<tr>
				<td class="facts_value">', $stats->oldestMarriageMale(), '</td>
				<td class="facts_value">', $stats->oldestMarriageFemale(), '</td>
			</tr>
			<tr>
				<td class="facts_value statistics-page" colspan="2">', $stats->statsMarrAge(), '</td>
			</tr>
		</table>
		<br>
		<b>', I18N::translate('Age at birth of child'), '</b>
		<table class="facts_table">
			<tr>
				<td class="facts_label">', I18N::translate('Youngest father'), ' - ', $stats->youngestFatherAge(true), '</td>
Esempio n. 3
0
    /**
     * Print a table of families
     *
     * @param Family[] $datalist
     *
     * @return string
     */
    public static function familyTable($datalist)
    {
        global $WT_TREE, $controller;
        $table_id = 'table-fam-' . Uuid::uuid4();
        // lists requires a unique ID in case there are multiple lists per page
        $controller->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)->addInlineJavascript('
				jQuery.fn.dataTableExt.oSort["unicode-asc" ]=function(a,b) {return a.replace(/<[^<]*>/, "").localeCompare(b.replace(/<[^<]*>/, ""))};
				jQuery.fn.dataTableExt.oSort["unicode-desc"]=function(a,b) {return b.replace(/<[^<]*>/, "").localeCompare(a.replace(/<[^<]*>/, ""))};
				jQuery("#' . $table_id . '").dataTable( {
					dom: \'<"H"<"filtersH_' . $table_id . '"><"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
					' . I18N::datatablesI18N() . ',
					jQueryUI: true,
					autoWidth: false,
					processing: true,
					retrieve: true,
					columns: [
						/*  0 husb givn */ {dataSort: 2},
						/*  1 husb surn */ {dataSort: 3},
						/*  2 GIVN,SURN */ {type: "unicode", visible: false},
						/*  3 SURN,GIVN */ {type: "unicode", visible: false},
						/*  4 age       */ {dataSort: 5, class: "center"},
						/*  5 AGE       */ {type: "num", visible: false},
						/*  6 wife givn */ {dataSort: 8},
						/*  7 wife surn */ {dataSort: 9},
						/*  8 GIVN,SURN */ {type: "unicode", visible: false},
						/*  9 SURN,GIVN */ {type: "unicode", visible: false},
						/* 10 age       */ {dataSort: 11, class: "center"},
						/* 11 AGE       */ {type: "num", visible: false},
						/* 12 marr date */ {dataSort: 13},
						/* 13 MARR:DATE */ {visible: false},
						/* 14 anniv     */ {dataSort: 13, class: "center"},
						/* 15 marr plac */ {type: "unicode"},
						/* 16 children  */ {dataSort: 17, class: "center"},
						/* 17 NCHI      */ {type: "num", visible: false},
						/* 18 CHAN      */ {dataSort: 19, visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . '},
						/* 19 CHAN_sort */ {visible: false},
						/* 20 MARR      */ {visible: false},
						/* 21 DEAT      */ {visible: false},
						/* 22 TREE      */ {visible: false}
					],
					sorting: [[1, "asc"]],
					displayLength: 20,
					pagingType: "full_numbers"
			   });

				jQuery("#' . $table_id . '")
				/* Hide/show parents */
				.on("click", ".btn-toggle-parents", function() {
					jQuery(this).toggleClass("ui-state-active");
					jQuery(".parents", jQuery(this).closest("table").DataTable().rows().nodes()).slideToggle();
				})
				/* Hide/show statistics */
				.on("click",  ".btn-toggle-statistics", function() {
					jQuery(this).toggleClass("ui-state-active");
					jQuery("#fam_list_table-charts_' . $table_id . '").slideToggle();
				})
				/* Filter buttons in table header */
				.on("click", "button[data-filter-column]", function() {
					var btn = $(this);
					// De-activate the other buttons in this button group
					btn.siblings().removeClass("ui-state-active");
					// Apply (or clear) this filter
					var col = jQuery("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
					if (btn.hasClass("ui-state-active")) {
						btn.removeClass("ui-state-active");
						col.search("").draw();
					} else {
						btn.addClass("ui-state-active");
						col.search(btn.data("filter-value")).draw();
					}
				});

				jQuery(".fam-list").css("visibility", "visible");
				jQuery(".loading-image").css("display", "none");
		');
        $stats = new Stats($WT_TREE);
        $max_age = max($stats->oldestMarriageMaleAge(), $stats->oldestMarriageFemaleAge()) + 1;
        //-- init chart data
        $marr_by_age = array();
        for ($age = 0; $age <= $max_age; $age++) {
            $marr_by_age[$age] = '';
        }
        $birt_by_decade = array();
        $marr_by_decade = array();
        for ($year = 1550; $year < 2030; $year += 10) {
            $birt_by_decade[$year] = '';
            $marr_by_decade[$year] = '';
        }
        $html = '
			<div class="loading-image">&nbsp;</div>
			<div class="fam-list">
				<table id="' . $table_id . '">
					<thead>
						<tr>
							<th colspan="23">
								<div class="btn-toolbar">
									<div class="btn-group">
										<button
											type="button"
											data-filter-column="21"
											data-filter-value="N"
											class="ui-state-default"
											title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') . '"
										>
											' . I18N::translate('Both alive') . '
										</button>
										<button
											type="button"
											data-filter-column="21"
											data-filter-value="W"
											class="ui-state-default"
											title="' . I18N::translate('Show couples where only the female partner is deceased.') . '"
										>
											' . I18N::translate('Widower') . '
										</button>
										<button
											type="button"
											data-filter-column="21"
											data-filter-value="H"
											class="ui-state-default"
											title="' . I18N::translate('Show couples where only the male partner is deceased.') . '"
										>
											' . I18N::translate('Widow') . '
										</button>
										<button
											type="button"
											data-filter-column="21"
											data-filter-value="Y"
											class="ui-state-default"
											title="' . I18N::translate('Show individuals who are dead or couples where both partners are deceased.') . '"
										>
											' . I18N::translate('Both dead') . '
										</button>
									</div>
									<div class="btn-group">
										<button
											type="button"
											data-filter-column="22"
											data-filter-value="R"
											class="ui-state-default"
											title="' . I18N::translate('Show “roots” couples or individuals.  These individuals may also be called “patriarchs”.  They are individuals who have no parents recorded in the database.') . '"
										>
											' . I18N::translate('Roots') . '
										</button>
										<button
											type="button"
											data-filter-column="22"
											data-filter-value="L"
											class="ui-state-default"
											title="' . I18N::translate('Show “leaves” couples or individuals.  These are individuals who are alive but have no children recorded in the database.') . '"
										>
											' . I18N::translate('Leaves') . '
										</button>
									</div>
									<div class="btn-group">
										<button
											type="button"
											data-filter-column="20"
											data-filter-value="U"
											class="ui-state-default"
											title="' . I18N::translate('Show couples with an unknown marriage date.') . '"
										>
											' . GedcomTag::getLabel('MARR') . '
										</button>
										<button
											type="button"
											data-filter-column="20"
											data-filter-value="YES"
											class="ui-state-default"
											title="' . I18N::translate('Show couples who married more than 100 years ago.') . '"
										>
											' . GedcomTag::getLabel('MARR') . '&gt;100
										</button>
										<button
											type="button"
											data-filter-column="20"
											data-filter-value="Y100"
											class="ui-state-default"
											title="' . I18N::translate('Show couples who married within the last 100 years.') . '"
										>
											' . GedcomTag::getLabel('MARR') . '&lt;=100
										</button>
										<button
											type="button"
											data-filter-column="20"
											data-filter-value="D"
											class="ui-state-default"
											title="' . I18N::translate('Show divorced couples.') . '"
										>
											' . GedcomTag::getLabel('DIV') . '
										</button>
										<button
											type="button"
											data-filter-column="20"
											data-filter-value="M"
											class="ui-state-default"
											title="' . I18N::translate('Show couples where either partner married more than once.') . '"
										>
											' . I18N::translate('Multiple marriages') . '
										</button>
									</div>
								</div>
							</th>
						</tr>
						<tr>
							<th>' . GedcomTag::getLabel('GIVN') . '</th>
							<th>' . GedcomTag::getLabel('SURN') . '</th>
							<th>HUSB:GIVN_SURN</th>
							<th>HUSB:SURN_GIVN</th>
							<th>' . GedcomTag::getLabel('AGE') . '</th>
							<th>AGE</th>
							<th>' . GedcomTag::getLabel('GIVN') . '</th>
							<th>' . GedcomTag::getLabel('SURN') . '</th>
							<th>WIFE:GIVN_SURN</th>
							<th>WIFE:SURN_GIVN</th>
							<th>' . GedcomTag::getLabel('AGE') . '</th>
							<th>AGE</th>
							<th>' . GedcomTag::getLabel('MARR') . '</th>
							<th>MARR:DATE</th>
							<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
							<th>' . GedcomTag::getLabel('PLAC') . '</th>
							<th><i class="icon-children" title="' . I18N::translate('Children') . '"></i></th>
						<th>NCHI</th>
						<th>' . GedcomTag::getLabel('CHAN') . '</th>
						<th>CHAN</th>
						<th>MARR</th>
						<th>DEAT</th>
						<th>TREE</th>
					</tr>
				</thead>
				<tfoot>
					<tr>
						<th colspan="23">
							<div class="btn-toolbar">
								<div class="btn-group">
									<button type="button" class="ui-state-default btn-toggle-parents">
										' . I18N::translate('Show parents') . '
									</button>
									<button type="button" class="ui-state-default btn-toggle-statistics">
										' . I18N::translate('Show statistics charts') . '
									</button>
								</div>
							</div>
						</th>
					</tr>
				</tfoot>
				<tbody>';
        $d100y = new Date(date('Y') - 100);
        // 100 years ago
        foreach ($datalist as $family) {
            //-- Retrieve husband and wife
            $husb = $family->getHusband();
            if (is_null($husb)) {
                $husb = new Individual('H', '0 @H@ INDI', null, $family->getTree());
            }
            $wife = $family->getWife();
            if (is_null($wife)) {
                $wife = new Individual('W', '0 @W@ INDI', null, $family->getTree());
            }
            if (!$family->canShow()) {
                continue;
            }
            if ($family->isPendingAddtion()) {
                $class = ' class="new"';
            } elseif ($family->isPendingDeletion()) {
                $class = ' class="old"';
            } else {
                $class = '';
            }
            $html .= '<tr' . $class . '>';
            //-- Husband name(s)
            $html .= '<td colspan="2">';
            foreach ($husb->getAllNames() as $num => $name) {
                if ($name['type'] == 'NAME') {
                    $title = '';
                } else {
                    $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $husb)) . '"';
                }
                if ($num == $husb->getPrimaryName()) {
                    $class = ' class="name2"';
                    $sex_image = $husb->getSexImage();
                    list($surn, $givn) = explode(',', $name['sort']);
                } else {
                    $class = '';
                    $sex_image = '';
                }
                // Only show married names if they are the name we are filtering by.
                if ($name['type'] != '_MARNM' || $num == $husb->getPrimaryName()) {
                    $html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
                }
            }
            // Husband parents
            $html .= $husb->getPrimaryParentsNames('parents details1', 'none');
            $html .= '</td>';
            // Dummy column to match colspan in header
            $html .= '<td style="display:none;"></td>';
            //-- Husb GIVN
            // Use "AAAA" as a separator (instead of ",") as Javascript.localeCompare() ignores
            // punctuation and "ANN,ROACH" would sort after "ANNE,ROACH", instead of before it.
            // Similarly, @N.N. would sort as NN.
            $html .= '<td>' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . 'AAAA' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . '</td>';
            $html .= '<td>' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . 'AAAA' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . '</td>';
            $mdate = $family->getMarriageDate();
            //-- Husband age
            $hdate = $husb->getBirthDate();
            if ($hdate->isOK() && $mdate->isOK()) {
                if ($hdate->gregorianYear() >= 1550 && $hdate->gregorianYear() < 2030) {
                    $birt_by_decade[(int) ($hdate->gregorianYear() / 10) * 10] .= $husb->getSex();
                }
                $hage = Date::getAge($hdate, $mdate, 0);
                if ($hage >= 0 && $hage <= $max_age) {
                    $marr_by_age[$hage] .= $husb->getSex();
                }
            }
            $html .= '<td>' . Date::getAge($hdate, $mdate, 2) . '</td><td>' . Date::getAge($hdate, $mdate, 1) . '</td>';
            //-- Wife name(s)
            $html .= '<td colspan="2">';
            foreach ($wife->getAllNames() as $num => $name) {
                if ($name['type'] == 'NAME') {
                    $title = '';
                } else {
                    $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $wife)) . '"';
                }
                if ($num == $wife->getPrimaryName()) {
                    $class = ' class="name2"';
                    $sex_image = $wife->getSexImage();
                    list($surn, $givn) = explode(',', $name['sort']);
                } else {
                    $class = '';
                    $sex_image = '';
                }
                // Only show married names if they are the name we are filtering by.
                if ($name['type'] != '_MARNM' || $num == $wife->getPrimaryName()) {
                    $html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
                }
            }
            // Wife parents
            $html .= $wife->getPrimaryParentsNames('parents details1', 'none');
            $html .= '</td>';
            // Dummy column to match colspan in header
            $html .= '<td style="display:none;"></td>';
            //-- Wife GIVN
            //-- Husb GIVN
            // Use "AAAA" as a separator (instead of ",") as Javascript.localeCompare() ignores
            // punctuation and "ANN,ROACH" would sort after "ANNE,ROACH", instead of before it.
            // Similarly, @N.N. would sort as NN.
            $html .= '<td>' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . 'AAAA' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . '</td>';
            $html .= '<td>' . Filter::escapeHtml(str_replace('@N.N.', 'AAAA', $surn)) . 'AAAA' . Filter::escapeHtml(str_replace('@P.N.', 'AAAA', $givn)) . '</td>';
            $mdate = $family->getMarriageDate();
            //-- Wife age
            $wdate = $wife->getBirthDate();
            if ($wdate->isOK() && $mdate->isOK()) {
                if ($wdate->gregorianYear() >= 1550 && $wdate->gregorianYear() < 2030) {
                    $birt_by_decade[(int) ($wdate->gregorianYear() / 10) * 10] .= $wife->getSex();
                }
                $wage = Date::getAge($wdate, $mdate, 0);
                if ($wage >= 0 && $wage <= $max_age) {
                    $marr_by_age[$wage] .= $wife->getSex();
                }
            }
            $html .= '<td>' . Date::getAge($wdate, $mdate, 2) . '</td><td>' . Date::getAge($wdate, $mdate, 1) . '</td>';
            //-- Marriage date
            $html .= '<td>';
            if ($marriage_dates = $family->getAllMarriageDates()) {
                foreach ($marriage_dates as $n => $marriage_date) {
                    if ($n) {
                        $html .= '<br>';
                    }
                    $html .= '<div>' . $marriage_date->display(true) . '</div>';
                }
                if ($marriage_dates[0]->gregorianYear() >= 1550 && $marriage_dates[0]->gregorianYear() < 2030) {
                    $marr_by_decade[(int) ($marriage_dates[0]->gregorianYear() / 10) * 10] .= $husb->getSex() . $wife->getSex();
                }
            } elseif ($family->getFacts('_NMR')) {
                $html .= I18N::translate('no');
            } elseif ($family->getFacts('MARR')) {
                $html .= I18N::translate('yes');
            } else {
                $html .= '&nbsp;';
            }
            $html .= '</td>';
            //-- Event date (sortable)hidden by datatables code
            $html .= '<td>';
            if ($marriage_dates) {
                $html .= $marriage_date->julianDay();
            } else {
                $html .= 0;
            }
            $html .= '</td>';
            //-- Marriage anniversary
            $html .= '<td>' . Date::getAge($mdate, null, 2) . '</td>';
            //-- Marriage place
            $html .= '<td>';
            foreach ($family->getAllMarriagePlaces() as $n => $marriage_place) {
                $tmp = new Place($marriage_place, $family->getTree());
                if ($n) {
                    $html .= '<br>';
                }
                $html .= '<a href="' . $tmp->getURL() . '" title="' . strip_tags($tmp->getFullName()) . '">';
                $html .= FunctionsPrint::highlightSearchHits($tmp->getShortName()) . '</a>';
            }
            $html .= '</td>';
            //-- Number of children
            $nchi = $family->getNumberOfChildren();
            $html .= '<td>' . I18N::number($nchi) . '</td><td>' . $nchi . '</td>';
            //-- Last change
            $html .= '<td>' . $family->LastChangeTimestamp() . '</td>';
            $html .= '<td>' . $family->LastChangeTimestamp(true) . '</td>';
            //-- Sorting by marriage date
            $html .= '<td>';
            if (!$family->canShow() || !$mdate->isOK()) {
                $html .= 'U';
            } else {
                if (Date::compare($mdate, $d100y) > 0) {
                    $html .= 'Y100';
                } else {
                    $html .= 'YES';
                }
            }
            if ($family->getFacts(WT_EVENTS_DIV)) {
                $html .= 'D';
            }
            if (count($husb->getSpouseFamilies()) > 1 || count($wife->getSpouseFamilies()) > 1) {
                $html .= 'M';
            }
            $html .= '</td>';
            //-- Sorting alive/dead
            $html .= '<td>';
            if ($husb->isDead() && $wife->isDead()) {
                $html .= 'Y';
            }
            if ($husb->isDead() && !$wife->isDead()) {
                if ($wife->getSex() == 'F') {
                    $html .= 'H';
                }
                if ($wife->getSex() == 'M') {
                    $html .= 'W';
                }
                // male partners
            }
            if (!$husb->isDead() && $wife->isDead()) {
                if ($husb->getSex() == 'M') {
                    $html .= 'W';
                }
                if ($husb->getSex() == 'F') {
                    $html .= 'H';
                }
                // female partners
            }
            if (!$husb->isDead() && !$wife->isDead()) {
                $html .= 'N';
            }
            $html .= '</td>';
            //-- Roots or Leaves
            $html .= '<td>';
            if (!$husb->getChildFamilies() && !$wife->getChildFamilies()) {
                $html .= 'R';
            } elseif (!$husb->isDead() && !$wife->isDead() && $family->getNumberOfChildren() < 1) {
                $html .= 'L';
            } else {
                $html .= '&nbsp;';
            }
            $html .= '</td>
			</tr>';
        }
        $html .= '
					</tbody>
				</table>
				<div id="fam_list_table-charts_' . $table_id . '" style="display:none">
					<table class="list-charts">
						<tr>
							<td>
								' . self::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')) . '
							</td>
							<td>
								' . self::chartByDecade($marr_by_decade, I18N::translate('Decade of marriage')) . '
							</td>
						</tr>
						<tr>
							<td colspan="2">
								' . self::chartByAge($marr_by_age, I18N::translate('Age in year of marriage')) . '
							</td>
						</tr>
					</table>
				</div>
			</div>';
        return $html;
    }
    /**
     * Print a table of families
     *
     * @param Family[] $families
     *
     * @return string
     */
    public static function familyTable($families)
    {
        global $WT_TREE, $controller;
        $table_id = 'table-fam-' . Uuid::uuid4();
        // lists requires a unique ID in case there are multiple lists per page
        $controller->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL)->addInlineJavascript('
				jQuery.fn.dataTableExt.oSort["text-asc"] = textCompareAsc;
				jQuery.fn.dataTableExt.oSort["text-desc"] = textCompareDesc;
				jQuery("#' . $table_id . '").dataTable( {
					dom: \'<"H"<"filtersH_' . $table_id . '"><"dt-clear">pf<"dt-clear">irl>t<"F"pl<"dt-clear"><"filtersF_' . $table_id . '">>\',
					' . I18N::datatablesI18N() . ',
					jQueryUI: true,
					autoWidth: false,
					processing: true,
					retrieve: true,
					columns: [
						/* Given names         */ { type: "text" },
						/* Surnames            */ { type: "text" },
						/* Age                 */ { type: "num" },
						/* Given names         */ { type: "text" },
						/* Surnames            */ { type: "text" },
						/* Age                 */ { type: "num" },
						/* Marriage date       */ { type: "num" },
						/* Anniversary         */ { type: "num" },
						/* Marriage place      */ { type: "text" },
						/* Children            */ { type: "num" },
						/* Last change         */ { visible: ' . ($WT_TREE->getPreference('SHOW_LAST_CHANGE') ? 'true' : 'false') . ' },
						/* Filter marriage     */ { sortable: false },
						/* Filter alive/dead   */ { sortable: false },
						/* Filter tree         */ { sortable: false }
					],
					sorting: [[1, "asc"]],
					displayLength: 20,
					pagingType: "full_numbers"
			   });

				jQuery("#' . $table_id . '")
				/* Hide/show parents */
				.on("click", ".btn-toggle-parents", function() {
					jQuery(this).toggleClass("ui-state-active");
					jQuery(".parents", jQuery(this).closest("table").DataTable().rows().nodes()).slideToggle();
				})
				/* Hide/show statistics */
				.on("click",  ".btn-toggle-statistics", function() {
					jQuery(this).toggleClass("ui-state-active");
					jQuery("#fam_list_table-charts_' . $table_id . '").slideToggle();
				})
				/* Filter buttons in table header */
				.on("click", "button[data-filter-column]", function() {
					var btn = $(this);
					// De-activate the other buttons in this button group
					btn.siblings().removeClass("ui-state-active");
					// Apply (or clear) this filter
					var col = jQuery("#' . $table_id . '").DataTable().column(btn.data("filter-column"));
					if (btn.hasClass("ui-state-active")) {
						btn.removeClass("ui-state-active");
						col.search("").draw();
					} else {
						btn.addClass("ui-state-active");
						col.search(btn.data("filter-value")).draw();
					}
				});

				jQuery(".fam-list").css("visibility", "visible");
				jQuery(".loading-image").css("display", "none");
		');
        $stats = new Stats($WT_TREE);
        $max_age = max($stats->oldestMarriageMaleAge(), $stats->oldestMarriageFemaleAge()) + 1;
        // init chart data
        $marr_by_age = array();
        for ($age = 0; $age <= $max_age; $age++) {
            $marr_by_age[$age] = '';
        }
        $birt_by_decade = array();
        $marr_by_decade = array();
        for ($year = 1550; $year < 2030; $year += 10) {
            $birt_by_decade[$year] = '';
            $marr_by_decade[$year] = '';
        }
        $html = '
			<div class="loading-image"></div>
			<div class="fam-list">
				<table id="' . $table_id . '">
					<thead>
						<tr>
							<th colspan="14">
								<div class="btn-toolbar">
									<div class="btn-group">
										<button
											type="button"
											data-filter-column="12"
											data-filter-value="N"
											class="ui-state-default"
											title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') . '"
										>
											' . I18N::translate('Both alive') . '
										</button>
										<button
											type="button"
											data-filter-column="12"
											data-filter-value="W"
											class="ui-state-default"
											title="' . I18N::translate('Show couples where only the female partner is dead.') . '"
										>
											' . I18N::translate('Widower') . '
										</button>
										<button
											type="button"
											data-filter-column="12"
											data-filter-value="H"
											class="ui-state-default"
											title="' . I18N::translate('Show couples where only the male partner is dead.') . '"
										>
											' . I18N::translate('Widow') . '
										</button>
										<button
											type="button"
											data-filter-column="12"
											data-filter-value="Y"
											class="ui-state-default"
											title="' . I18N::translate('Show individuals who are dead or couples where both partners are dead.') . '"
										>
											' . I18N::translate('Both dead') . '
										</button>
									</div>
									<div class="btn-group">
										<button
											type="button"
											data-filter-column="13"
											data-filter-value="R"
											class="ui-state-default"
											title="' . I18N::translate('Show “roots” couples or individuals. These individuals may also be called “patriarchs”. They are individuals who have no parents recorded in the database.') . '"
										>
											' . I18N::translate('Roots') . '
										</button>
										<button
											type="button"
											data-filter-column="13"
											data-filter-value="L"
											class="ui-state-default"
											title="' . I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') . '"
										>
											' . I18N::translate('Leaves') . '
										</button>
									</div>
									<div class="btn-group">
										<button
											type="button"
											data-filter-column="11"
											data-filter-value="U"
											class="ui-state-default"
											title="' . I18N::translate('Show couples with an unknown marriage date.') . '"
										>
											' . GedcomTag::getLabel('MARR') . '
										</button>
										<button
											type="button"
											data-filter-column="11"
											data-filter-value="YES"
											class="ui-state-default"
											title="' . I18N::translate('Show couples who married more than 100 years ago.') . '"
										>
											' . GedcomTag::getLabel('MARR') . '&gt;100
										</button>
										<button
											type="button"
											data-filter-column="11"
											data-filter-value="Y100"
											class="ui-state-default"
											title="' . I18N::translate('Show couples who married within the last 100 years.') . '"
										>
											' . GedcomTag::getLabel('MARR') . '&lt;=100
										</button>
										<button
											type="button"
											data-filter-column="11"
											data-filter-value="D"
											class="ui-state-default"
											title="' . I18N::translate('Show divorced couples.') . '"
										>
											' . GedcomTag::getLabel('DIV') . '
										</button>
										<button
											type="button"
											data-filter-column="11"
											data-filter-value="M"
											class="ui-state-default"
											title="' . I18N::translate('Show couples where either partner married more than once.') . '"
										>
											' . I18N::translate('Multiple marriages') . '
										</button>
									</div>
								</div>
							</th>
						</tr>
						<tr>
							<th>' . GedcomTag::getLabel('GIVN') . '</th>
							<th>' . GedcomTag::getLabel('SURN') . '</th>
							<th>' . GedcomTag::getLabel('AGE') . '</th>
							<th>' . GedcomTag::getLabel('GIVN') . '</th>
							<th>' . GedcomTag::getLabel('SURN') . '</th>
							<th>' . GedcomTag::getLabel('AGE') . '</th>
							<th>' . GedcomTag::getLabel('MARR') . '</th>
							<th><i class="icon-reminder" title="' . I18N::translate('Anniversary') . '"></i></th>
							<th>' . GedcomTag::getLabel('PLAC') . '</th>
							<th><i class="icon-children" title="' . I18N::translate('Children') . '"></i></th>
							<th>' . GedcomTag::getLabel('CHAN') . '</th>
							<th hidden></th>
							<th hidden></th>
							<th hidden></th>
						</tr>
					</thead>
					<tfoot>
						<tr>
							<th colspan="14">
								<div class="btn-toolbar">
									<div class="btn-group">
										<button type="button" class="ui-state-default btn-toggle-parents">
											' . I18N::translate('Show parents') . '
										</button>
										<button type="button" class="ui-state-default btn-toggle-statistics">
											' . I18N::translate('Show statistics charts') . '
										</button>
									</div>
								</div>
							</th>
						</tr>
					</tfoot>
					<tbody>';
        $hundred_years_ago = new Date(date('Y') - 100);
        foreach ($families as $family) {
            // Retrieve husband and wife
            $husb = $family->getHusband();
            if (is_null($husb)) {
                $husb = new Individual('H', '0 @H@ INDI', null, $family->getTree());
            }
            $wife = $family->getWife();
            if (is_null($wife)) {
                $wife = new Individual('W', '0 @W@ INDI', null, $family->getTree());
            }
            if (!$family->canShow()) {
                continue;
            }
            if ($family->isPendingAddtion()) {
                $class = ' class="new"';
            } elseif ($family->isPendingDeletion()) {
                $class = ' class="old"';
            } else {
                $class = '';
            }
            $html .= '<tr' . $class . '>';
            // Husband name(s)
            // Extract Given names and Surnames for sorting
            list($surn_givn, $givn_surn) = self::sortableNames($husb);
            $html .= '<td colspan="2" data-sort="' . Filter::escapeHtml($givn_surn) . '">';
            foreach ($husb->getAllNames() as $num => $name) {
                if ($name['type'] == 'NAME') {
                    $title = '';
                } else {
                    $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $husb)) . '"';
                }
                if ($num == $husb->getPrimaryName()) {
                    $class = ' class="name2"';
                    $sex_image = $husb->getSexImage();
                } else {
                    $class = '';
                    $sex_image = '';
                }
                // Only show married names if they are the name we are filtering by.
                if ($name['type'] != '_MARNM' || $num == $husb->getPrimaryName()) {
                    $html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
                }
            }
            // Husband parents
            $html .= $husb->getPrimaryParentsNames('parents details1', 'none');
            $html .= '</td>';
            // Hidden column for sortable name
            $html .= '<td hidden data-sort="' . Filter::escapeHtml($surn_givn) . '"></td>';
            // Husband age
            $mdate = $family->getMarriageDate();
            $hdate = $husb->getBirthDate();
            if ($hdate->isOK() && $mdate->isOK()) {
                if ($hdate->gregorianYear() >= 1550 && $hdate->gregorianYear() < 2030) {
                    $birt_by_decade[(int) ($hdate->gregorianYear() / 10) * 10] .= $husb->getSex();
                }
                $hage = Date::getAge($hdate, $mdate, 0);
                if ($hage >= 0 && $hage <= $max_age) {
                    $marr_by_age[$hage] .= $husb->getSex();
                }
            }
            $html .= '<td class="center" data=-sort="' . Date::getAge($hdate, $mdate, 1) . '">' . Date::getAge($hdate, $mdate, 2) . '</td>';
            // Wife name(s)
            // Extract Given names and Surnames for sorting
            list($surn_givn, $givn_surn) = self::sortableNames($wife);
            $html .= '<td colspan="2" data-sort="' . Filter::escapeHtml($givn_surn) . '">';
            foreach ($wife->getAllNames() as $num => $name) {
                if ($name['type'] == 'NAME') {
                    $title = '';
                } else {
                    $title = 'title="' . strip_tags(GedcomTag::getLabel($name['type'], $wife)) . '"';
                }
                if ($num == $wife->getPrimaryName()) {
                    $class = ' class="name2"';
                    $sex_image = $wife->getSexImage();
                } else {
                    $class = '';
                    $sex_image = '';
                }
                // Only show married names if they are the name we are filtering by.
                if ($name['type'] != '_MARNM' || $num == $wife->getPrimaryName()) {
                    $html .= '<a ' . $title . ' href="' . $family->getHtmlUrl() . '"' . $class . '>' . FunctionsPrint::highlightSearchHits($name['full']) . '</a>' . $sex_image . '<br>';
                }
            }
            // Wife parents
            $html .= $wife->getPrimaryParentsNames('parents details1', 'none');
            $html .= '</td>';
            // Hidden column for sortable name
            $html .= '<td hidden data-sort="' . Filter::escapeHtml($surn_givn) . '"></td>';
            // Wife age
            $mdate = $family->getMarriageDate();
            $wdate = $wife->getBirthDate();
            if ($wdate->isOK() && $mdate->isOK()) {
                if ($wdate->gregorianYear() >= 1550 && $wdate->gregorianYear() < 2030) {
                    $birt_by_decade[(int) ($wdate->gregorianYear() / 10) * 10] .= $wife->getSex();
                }
                $wage = Date::getAge($wdate, $mdate, 0);
                if ($wage >= 0 && $wage <= $max_age) {
                    $marr_by_age[$wage] .= $wife->getSex();
                }
            }
            $html .= '<td class="center" data-sort="' . Date::getAge($wdate, $mdate, 1) . '">' . Date::getAge($wdate, $mdate, 2) . '</td>';
            // Marriage date
            $html .= '<td data-sort="' . $family->getMarriageDate()->julianDay() . '">';
            if ($marriage_dates = $family->getAllMarriageDates()) {
                foreach ($marriage_dates as $n => $marriage_date) {
                    if ($n) {
                        $html .= '<br>';
                    }
                    $html .= '<div>' . $marriage_date->display(true) . '</div>';
                }
                if ($marriage_dates[0]->gregorianYear() >= 1550 && $marriage_dates[0]->gregorianYear() < 2030) {
                    $marr_by_decade[(int) ($marriage_dates[0]->gregorianYear() / 10) * 10] .= $husb->getSex() . $wife->getSex();
                }
            } elseif ($family->getFacts('_NMR')) {
                $html .= I18N::translate('no');
            } elseif ($family->getFacts('MARR')) {
                $html .= I18N::translate('yes');
            } else {
                $html .= '&nbsp;';
            }
            $html .= '</td>';
            // Marriage anniversary
            $html .= '<td class="center" data-sort="' . -$family->getMarriageDate()->julianDay() . '">' . Date::getAge($family->getMarriageDate(), null, 2) . '</td>';
            // Marriage place
            $html .= '<td>';
            foreach ($family->getAllMarriagePlaces() as $n => $marriage_place) {
                $tmp = new Place($marriage_place, $family->getTree());
                if ($n) {
                    $html .= '<br>';
                }
                $html .= '<a href="' . $tmp->getURL() . '" title="' . strip_tags($tmp->getFullName()) . '">';
                $html .= FunctionsPrint::highlightSearchHits($tmp->getShortName()) . '</a>';
            }
            $html .= '</td>';
            // Number of children
            $html .= '<td class="center" data-sort="' . $family->getNumberOfChildren() . '">' . I18N::number($family->getNumberOfChildren()) . '</td>';
            // Last change
            $html .= '<td data-sort="' . $family->lastChangeTimestamp(true) . '">' . $family->lastChangeTimestamp() . '</td>';
            // Filter by marriage date
            $html .= '<td hidden>';
            if (!$family->canShow() || !$mdate->isOK()) {
                $html .= 'U';
            } else {
                if (Date::compare($mdate, $hundred_years_ago) > 0) {
                    $html .= 'Y100';
                } else {
                    $html .= 'YES';
                }
            }
            if ($family->getFacts(WT_EVENTS_DIV)) {
                $html .= 'D';
            }
            if (count($husb->getSpouseFamilies()) > 1 || count($wife->getSpouseFamilies()) > 1) {
                $html .= 'M';
            }
            $html .= '</td>';
            // Filter by alive/dead
            $html .= '<td hidden>';
            if ($husb->isDead() && $wife->isDead()) {
                $html .= 'Y';
            }
            if ($husb->isDead() && !$wife->isDead()) {
                if ($wife->getSex() == 'F') {
                    $html .= 'H';
                }
                if ($wife->getSex() == 'M') {
                    $html .= 'W';
                }
                // male partners
            }
            if (!$husb->isDead() && $wife->isDead()) {
                if ($husb->getSex() == 'M') {
                    $html .= 'W';
                }
                if ($husb->getSex() == 'F') {
                    $html .= 'H';
                }
                // female partners
            }
            if (!$husb->isDead() && !$wife->isDead()) {
                $html .= 'N';
            }
            $html .= '</td>';
            // Filter by roots/leaves
            $html .= '<td hidden>';
            if (!$husb->getChildFamilies() && !$wife->getChildFamilies()) {
                $html .= 'R';
            } elseif (!$husb->isDead() && !$wife->isDead() && $family->getNumberOfChildren() === 0) {
                $html .= 'L';
            }
            $html .= '</td>
			</tr>';
        }
        $html .= '
					</tbody>
				</table>
				<div id="fam_list_table-charts_' . $table_id . '" style="display:none">
					<table class="list-charts">
						<tr>
							<td>' . self::chartByDecade($birt_by_decade, I18N::translate('Decade of birth')) . '</td>
							<td>' . self::chartByDecade($marr_by_decade, I18N::translate('Decade of marriage')) . '</td>
						</tr>
						<tr>
							<td colspan="2">' . self::chartByAge($marr_by_age, I18N::translate('Age in year of marriage')) . '</td>
						</tr>
					</table>
				</div>
			</div>';
        return $html;
    }