. */ namespace Fisharebest\Webtrees; use Fisharebest\Webtrees\Functions\Functions; use Fisharebest\Webtrees\Functions\FunctionsDate; use Fisharebest\Webtrees\Functions\FunctionsDb; use Fisharebest\Webtrees\Functions\FunctionsPrint; use Fisharebest\Webtrees\Functions\FunctionsPrintLists; use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule; use Fisharebest\Webtrees\Module\UserFavoritesModule; use Fisharebest\Webtrees\Query\QueryName; use PDO; use PDOException; use Rhumsaa\Uuid\Uuid; /** * A selection of pre-formatted statistical queries. * * These are primarily used for embedded keywords on HTML blocks, but * are also used elsewhere in the code. */ class Stats { /** @var Tree Generate statistics for a specified tree. */ private $tree; /** @var string[] All public functions are available as keywords - except these ones */ private $public_but_not_allowed = array( '__construct', 'embedTags', 'iso3166', 'getAllCountries', 'getAllTagsTable', 'getAllTagsText', 'statsPlaces', 'statsBirthQuery', 'statsDeathQuery', 'statsMarrQuery', 'statsAgeQuery', 'monthFirstChildQuery', 'statsChildrenQuery', 'statsMarrAgeQuery', ); /** @var string[] List of GEDCOM media types */ private $_media_types = array('audio', 'book', 'card', 'certificate', 'coat', 'document', 'electronic', 'magazine', 'manuscript', 'map', 'fiche', 'film', 'newspaper', 'painting', 'photo', 'tombstone', 'video', 'other'); /** * Create the statistics for a tree. * * @param Tree $tree Generate statistics for this tree */ public function __construct(Tree $tree) { $this->tree = $tree; } /** * Return a string of all supported tags and an example of its output in table row form. * * @return string */ public function getAllTagsTable() { $examples = array(); foreach (get_class_methods($this) as $method) { $reflection = new \ReflectionMethod($this, $method); if ($reflection->isPublic() && !in_array($method, $this->public_but_not_allowed)) { $examples[$method] = $this->$method(); } } ksort($examples); $html = ''; foreach ($examples as $tag => $value) { $html .= ''; $html .= '' . $tag . ''; $html .= '' . $value . ''; $html .= ''; } return '' . '' . '' . '' . '' . '' . $html . '
' . I18N::translate('Embedded variable') . '' . I18N::translate('Resulting value') . '
'; } /** * Return a string of all supported tags in plain text. * * @return string */ public function getAllTagsText() { $examples = array(); foreach (get_class_methods($this) as $method) { $reflection = new \ReflectionMethod($this, $method); if ($reflection->isPublic() && !in_array($method, $this->public_but_not_allowed)) { $examples[$method] = $method; } } ksort($examples); return implode('
', $examples); } /** * Get tags and their parsed results. * * @param string $text * * @return string[][] */ private function getTags($text) { static $funcs; // Retrive all class methods isset($funcs) or $funcs = get_class_methods($this); // Extract all tags from the provided text preg_match_all("/#([^#]+)(?=#)/", (string) $text, $match); $tags = $match[1]; $c = count($tags); $new_tags = array(); // tag to replace $new_values = array(); // value to replace it with /* * Parse block tags. */ for ($i = 0; $i < $c; $i++) { $full_tag = $tags[$i]; // Added for new parameter support $params = explode(':', $tags[$i]); if (count($params) > 1) { $tags[$i] = array_shift($params); } else { $params = array(); } // Generate the replacement value for the tag if (method_exists($this, $tags[$i])) { $new_tags[] = "#{$full_tag}#"; $new_values[] = call_user_func_array(array($this, $tags[$i]), array($params)); } } return array($new_tags, $new_values); } /** * Embed tags in text * * @param string $text * * @return string */ public function embedTags($text) { if (strpos($text, '#') !== false) { list($new_tags, $new_values) = $this->getTags($text); $text = str_replace($new_tags, $new_values, $text); } return $text; } /** * Get the name used for GEDCOM files and URLs. * * @return string */ public function gedcomFilename() { return $this->tree->getName(); } /** * Get the internal ID number of the tree. * * @return int */ public function gedcomId() { return $this->tree->getTreeId(); } /** * Get the descriptive title of the tree. * * @return string */ public function gedcomTitle() { return $this->tree->getTitleHtml(); } /** * Get information from the GEDCOM's HEAD record. * * @return string[] */ private function gedcomHead() { $title = ''; $version = ''; $source = ''; $head = GedcomRecord::getInstance('HEAD', $this->tree); $sour = $head->getFirstFact('SOUR'); if ($sour) { $source = $sour->getValue(); $title = $sour->getAttribute('NAME'); $version = $sour->getAttribute('VERS'); } return array($title, $version, $source); } /** * Get the software originally used to create the GEDCOM file. * * @return string */ public function gedcomCreatedSoftware() { $head = $this->gedcomHead(); return $head[0]; } /** * Get the version of software which created the GEDCOM file. * * @return string */ public function gedcomCreatedVersion() { $head = $this->gedcomHead(); // fix broken version string in Family Tree Maker if (strstr($head[1], 'Family Tree Maker ')) { $p = strpos($head[1], '(') + 1; $p2 = strpos($head[1], ')'); $head[1] = substr($head[1], $p, ($p2 - $p)); } // Fix EasyTree version if ($head[2] == 'EasyTree') { $head[1] = substr($head[1], 1); } return $head[1]; } /** * Get the date the GEDCOM file was created. * * @return string */ public function gedcomDate() { $head = GedcomRecord::getInstance('HEAD', $this->tree); $fact = $head->getFirstFact('DATE'); if ($fact) { $date = new Date($fact->getValue()); return $date->display(); } return ''; } /** * When was this tree last updated? * * @return string */ public function gedcomUpdated() { $row = Database::prepare( "SELECT SQL_CACHE d_year, d_month, d_day FROM `##dates` WHERE d_julianday1 = (SELECT MAX(d_julianday1) FROM `##dates` WHERE d_file =? AND d_fact='CHAN') LIMIT 1" )->execute(array($this->tree->getTreeId()))->fetchOneRow(); if ($row) { $date = new Date("{$row->d_day} {$row->d_month} {$row->d_year}"); return $date->display(); } else { return $this->gedcomDate(); } } /** * What is the significant individual from this tree? * * @return string */ public function gedcomRootId() { return $this->tree->getPreference('PEDIGREE_ROOT_ID'); } /** * Convert totals into percentages. * * @param string $total * @param string $type * * @return string */ private function getPercentage($total, $type) { switch ($type) { case 'individual': $type = $this->totalIndividualsQuery(); break; case 'family': $type = $this->totalFamiliesQuery(); break; case 'source': $type = $this->totalSourcesQuery(); break; case 'note': $type = $this->totalNotesQuery(); break; case 'all': default: $type = $this->totalIndividualsQuery() + $this->totalFamiliesQuery() + $this->totalSourcesQuery(); break; } if ($type == 0) { return I18N::percentage(0, 1); } else { return I18N::percentage($total / $type, 1); } } /** * How many GEDCOM records exist in the tree. * * @return string */ public function totalRecords() { return I18N::number($this->totalIndividualsQuery() + $this->totalFamiliesQuery() + $this->totalSourcesQuery()); } /** * How many individuals exist in the tree. * * @return int */ private function totalIndividualsQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##individuals` WHERE i_file = :tree_id" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * How many individuals exist in the tree. * * @return string */ public function totalIndividuals() { return I18N::number($this->totalIndividualsQuery()); } /** * How many individuals have one or more sources. * * @return int */ private function totalIndisWithSourcesQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(DISTINCT i_id)" . " FROM `##individuals` JOIN `##link` ON i_id = l_from AND i_file = l_file" . " WHERE l_file = :tree_id AND l_type = 'SOUR'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * How many individuals have one or more sources. * * @return string */ public function totalIndisWithSources() { return I18N::number($this->totalIndisWithSourcesQuery()); } /** * Create a chart showing individuals with/without sources. * * @param string[] $params * * @return string */ public function chartIndisWithSources($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } $sizes = explode('x', $size); $tot_indi = $this->totalIndividualsQuery(); if ($tot_indi == 0) { return ''; } else { $tot_sindi_per = round($this->totalIndisWithSourcesQuery() / $tot_indi, 3); $chd = $this->arrayToExtendedEncoding(array(100 - 100 * $tot_sindi_per, 100 * $tot_sindi_per)); $chl = I18N::translate('Without sources') . ' - ' . I18N::percentage(1 - $tot_sindi_per, 1) . '|' . I18N::translate('With sources') . ' - ' . I18N::percentage($tot_sindi_per, 1); $chart_title = I18N::translate('Individuals with sources'); return '' . $chart_title . ''; } } /** * Show the total individuals as a percentage. * * @return string */ public function totalIndividualsPercentage() { return $this->getPercentage($this->totalIndividualsQuery(), 'all'); } /** * Count the total families. * * @return int */ private function totalFamiliesQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##families` WHERE f_file = :tree_id" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the total families. * * @return string */ public function totalFamilies() { return I18N::number($this->totalFamiliesQuery()); } /** * Count the families with source records. * * @return int */ private function totalFamsWithSourcesQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(DISTINCT f_id)" . " FROM `##families` JOIN `##link` ON f_id = l_from AND f_file = l_file" . " WHERE l_file = :tree_id AND l_type = 'SOUR'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the families with with source records. * * @return string */ public function totalFamsWithSources() { return I18N::number($this->totalFamsWithSourcesQuery()); } /** * Create a chart of individuals with/without sources. * * @param string[] $params * * @return string */ public function chartFamsWithSources($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } $sizes = explode('x', $size); $tot_fam = $this->totalFamiliesQuery(); if ($tot_fam == 0) { return ''; } else { $tot_sfam_per = round($this->totalFamsWithSourcesQuery() / $tot_fam, 3); $chd = $this->arrayToExtendedEncoding(array(100 - 100 * $tot_sfam_per, 100 * $tot_sfam_per)); $chl = I18N::translate('Without sources') . ' - ' . I18N::percentage(1 - $tot_sfam_per, 1) . '|' . I18N::translate('With sources') . ' - ' . I18N::percentage($tot_sfam_per, 1); $chart_title = I18N::translate('Families with sources'); return "\"""; } } /** * Show the total families as a percentage. * * @return string */ public function totalFamiliesPercentage() { return $this->getPercentage($this->totalFamiliesQuery(), 'all'); } /** * Count the total number of sources. * * @return int */ private function totalSourcesQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##sources` WHERE s_file = :tree_id" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the total number of sources. * * @return string */ public function totalSources() { return I18N::number($this->totalSourcesQuery()); } /** * Show the number of sources as a percentage. * * @return string */ public function totalSourcesPercentage() { return $this->getPercentage($this->totalSourcesQuery(), 'all'); } /** * Count the number of notes. * * @return int */ private function totalNotesQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##other` WHERE o_type='NOTE' AND o_file = :tree_id" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the number of notes. * * @return string */ public function totalNotes() { return I18N::number($this->totalNotesQuery()); } /** * Show the number of notes as a percentage. * * @return string */ public function totalNotesPercentage() { return $this->getPercentage($this->totalNotesQuery(), 'all'); } /** * Count the number of repositories. * * @return int */ private function totalRepositoriesQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##other` WHERE o_type='REPO' AND o_file = :tree_id" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the number of repositories * * @return string */ public function totalRepositories() { return I18N::number($this->totalRepositoriesQuery()); } /** * Show the total number of repositories as a percentage. * * @return string */ public function totalRepositoriesPercentage() { return $this->getPercentage($this->totalRepositoriesQuery(), 'all'); } /** * Count the surnames. * * @param string[] $params * * @return string */ public function totalSurnames($params = array()) { if ($params) { $opt = 'IN (' . implode(',', array_fill(0, count($params), '?')) . ')'; $distinct = ''; } else { $opt = "IS NOT NULL"; $distinct = 'DISTINCT'; } $params[] = $this->tree->getTreeId(); $total = Database::prepare( "SELECT SQL_CACHE COUNT({$distinct} n_surn COLLATE '" . I18N::collation() . "')" . " FROM `##name`" . " WHERE n_surn COLLATE '" . I18N::collation() . "' {$opt} AND n_file=?" )->execute( $params )->fetchOne(); return I18N::number($total); } /** * Count the number of distinct given names, or count the number of * occurrences of a specific name or names. * * @param string[] $params * * @return string */ public function totalGivennames($params = array()) { if ($params) { $qs = implode(',', array_fill(0, count($params), '?')); $params[] = $this->tree->getTreeId(); $total = Database::prepare("SELECT SQL_CACHE COUNT( n_givn) FROM `##name` WHERE n_givn IN ({$qs}) AND n_file=?") ->execute($params) ->fetchOne(); } else { $total = Database::prepare("SELECT SQL_CACHE COUNT(DISTINCT n_givn) FROM `##name` WHERE n_givn IS NOT NULL AND n_file=?") ->execute(array($this->tree->getTreeId())) ->fetchOne(); } return I18N::number($total); } /** * Count the number of events (with dates). * * @param string[] $params * * @return string */ public function totalEvents($params = array()) { $sql = "SELECT SQL_CACHE COUNT(*) AS tot FROM `##dates` WHERE d_file=?"; $vars = array($this->tree->getTreeId()); $no_types = array('HEAD', 'CHAN'); if ($params) { $types = array(); foreach ($params as $type) { if (substr($type, 0, 1) == '!') { $no_types[] = substr($type, 1); } else { $types[] = $type; } } if ($types) { $sql .= ' AND d_fact IN (' . implode(', ', array_fill(0, count($types), '?')) . ')'; $vars = array_merge($vars, $types); } } $sql .= ' AND d_fact NOT IN (' . implode(', ', array_fill(0, count($no_types), '?')) . ')'; $vars = array_merge($vars, $no_types); return I18N::number(Database::prepare($sql)->execute($vars)->fetchOne()); } /** * Count the number of births. * * @return string */ public function totalEventsBirth() { return $this->totalEvents(explode('|', WT_EVENTS_BIRT)); } /** * Count the number of births. * * @return string */ public function totalBirths() { return $this->totalEvents(array('BIRT')); } /** * Count the number of deaths. * * @return string */ public function totalEventsDeath() { return $this->totalEvents(explode('|', WT_EVENTS_DEAT)); } /** * Count the number of deaths. * * @return string */ public function totalDeaths() { return $this->totalEvents(array('DEAT')); } /** * Count the number of marriages. * * @return string */ public function totalEventsMarriage() { return $this->totalEvents(explode('|', WT_EVENTS_MARR)); } /** * Count the number of marriages. * * @return string */ public function totalMarriages() { return $this->totalEvents(array('MARR')); } /** * Count the number of divorces. * * @return string */ public function totalEventsDivorce() { return $this->totalEvents(explode('|', WT_EVENTS_DIV)); } /** * Count the number of divorces. * * @return string */ public function totalDivorces() { return $this->totalEvents(array('DIV')); } /** * Count the number of other events. * * @return string */ public function totalEventsOther() { $facts = array_merge(explode('|', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT)); $no_facts = array(); foreach ($facts as $fact) { $fact = '!' . str_replace('\'', '', $fact); $no_facts[] = $fact; } return $this->totalEvents($no_facts); } /** * Count the number of males. * * @return int */ private function totalSexMalesQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'M'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the number of males. * * @return string */ public function totalSexMales() { return I18N::number($this->totalSexMalesQuery()); } /** * Count the number of males * * @return string */ public function totalSexMalesPercentage() { return $this->getPercentage($this->totalSexMalesQuery(), 'individual'); } /** * Count the number of females. * * @return int */ private function totalSexFemalesQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'F'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the number of females. * * @return string */ public function totalSexFemales() { return I18N::number($this->totalSexFemalesQuery()); } /** * Count the number of females. * * @return string */ public function totalSexFemalesPercentage() { return $this->getPercentage($this->totalSexFemalesQuery(), 'individual'); } /** * Count the number of individuals with unknown sex. * * @return int */ private function totalSexUnknownQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'U'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the number of individuals with unknown sex. * * @return string */ public function totalSexUnknown() { return I18N::number($this->totalSexUnknownQuery()); } /** * Count the number of individuals with unknown sex. * * @return string */ public function totalSexUnknownPercentage() { return $this->getPercentage($this->totalSexUnknownQuery(), 'individual'); } /** * Generate a chart showing sex distribution. * * @param string[] $params * * @return string */ public function chartSex($params = array()) { $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_female = strtolower($params[1]); } else { $color_female = 'ffd1dc'; } if (isset($params[2]) && $params[2] != '') { $color_male = strtolower($params[2]); } else { $color_male = '84beff'; } if (isset($params[3]) && $params[3] != '') { $color_unknown = strtolower($params[3]); } else { $color_unknown = '777777'; } $sizes = explode('x', $size); // Raw data - for calculation $tot_f = $this->totalSexFemalesQuery(); $tot_m = $this->totalSexMalesQuery(); $tot_u = $this->totalSexUnknownQuery(); $tot = $tot_f + $tot_m + $tot_u; // I18N data - for display $per_f = $this->totalSexFemalesPercentage(); $per_m = $this->totalSexMalesPercentage(); $per_u = $this->totalSexUnknownPercentage(); if ($tot == 0) { return ''; } elseif ($tot_u > 0) { $chd = $this->arrayToExtendedEncoding(array(4095 * $tot_u / $tot, 4095 * $tot_f / $tot, 4095 * $tot_m / $tot)); $chl = I18N::translateContext('unknown people', 'Unknown') . ' - ' . $per_u . '|' . I18N::translate('Females') . ' - ' . $per_f . '|' . I18N::translate('Males') . ' - ' . $per_m; $chart_title = I18N::translate('Males') . ' - ' . $per_m . I18N::$list_separator . I18N::translate('Females') . ' - ' . $per_f . I18N::$list_separator . I18N::translateContext('unknown people', 'Unknown') . ' - ' . $per_u; return "\"""; } else { $chd = $this->arrayToExtendedEncoding(array(4095 * $tot_f / $tot, 4095 * $tot_m / $tot)); $chl = I18N::translate('Females') . ' - ' . $per_f . '|' . I18N::translate('Males') . ' - ' . $per_m; $chart_title = I18N::translate('Males') . ' - ' . $per_m . I18N::$list_separator . I18N::translate('Females') . ' - ' . $per_f; return "\"""; } } /** * Count the number of living individuals. * * The totalLiving/totalDeceased queries assume that every dead person will * have a DEAT record. It will not include individuals who were born more * than MAX_ALIVE_AGE years ago, and who have no DEAT record. * A good reason to run the “Add missing DEAT records” batch-update! * * @return int */ private function totalLivingQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom NOT REGEXP '\\n1 (" . WT_EVENTS_DEAT . ")'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the number of living individuals. * * @return string */ public function totalLiving() { return I18N::number($this->totalLivingQuery()); } /** * Count the number of living individuals. * * @return string */ public function totalLivingPercentage() { return $this->getPercentage($this->totalLivingQuery(), 'individual'); } /** * Count the number of dead individuals. * * @return int */ private function totalDeceasedQuery() { return (int) Database::prepare( "SELECT SQL_CACHE COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom REGEXP '\\n1 (" . WT_EVENTS_DEAT . ")'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchOne(); } /** * Count the number of dead individuals. * * @return string */ public function totalDeceased() { return I18N::number($this->totalDeceasedQuery()); } /** * Count the number of dead individuals. * * @return string */ public function totalDeceasedPercentage() { return $this->getPercentage($this->totalDeceasedQuery(), 'individual'); } /** * Create a chart showing mortality. * * @param string[] $params * * @return string */ public function chartMortality($params = array()) { $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_living = strtolower($params[1]); } else { $color_living = 'ffffff'; } if (isset($params[2]) && $params[2] != '') { $color_dead = strtolower($params[2]); } else { $color_dead = 'cccccc'; } $sizes = explode('x', $size); // Raw data - for calculation $tot_l = $this->totalLivingQuery(); $tot_d = $this->totalDeceasedQuery(); $tot = $tot_l + $tot_d; // I18N data - for display $per_l = $this->totalLivingPercentage(); $per_d = $this->totalDeceasedPercentage(); if ($tot == 0) { return ''; } else { $chd = $this->arrayToExtendedEncoding(array(4095 * $tot_l / $tot, 4095 * $tot_d / $tot)); $chl = I18N::translate('Living') . ' - ' . $per_l . '|' . I18N::translate('Dead') . ' - ' . $per_d . '|'; $chart_title = I18N::translate('Living') . ' - ' . $per_l . I18N::$list_separator . I18N::translate('Dead') . ' - ' . $per_d; return "\"""; } } /** * Count the number of users. * * @param string[] $params * * @return string */ public function totalUsers($params = array()) { if (isset($params[0])) { $total = count(User::all()) + (int) $params[0]; } else { $total = count(User::all()); } return I18N::number($total); } /** * Count the number of administrators. * * @return string */ public function totalAdmins() { return I18N::number(count(User::allAdmins())); } /** * Count the number of administrators. * * @return string */ public function totalNonAdmins() { return I18N::number(count(User::all()) - count(User::allAdmins())); } /** * Count the number of media records with a given type. * * @param string $type * * @return int */ private function totalMediaType($type = 'all') { if (!in_array($type, $this->_media_types) && $type != 'all' && $type != 'unknown') { return 0; } $sql = "SELECT SQL_CACHE COUNT(*) AS tot FROM `##media` WHERE m_file=?"; $vars = array($this->tree->getTreeId()); if ($type != 'all') { if ($type == 'unknown') { // There has to be a better way then this :( foreach ($this->_media_types as $t) { $sql .= " AND (m_gedcom NOT LIKE ? AND m_gedcom NOT LIKE ?)"; $vars[] = "%3 TYPE {$t}%"; $vars[] = "%1 _TYPE {$t}%"; } } else { $sql .= " AND (m_gedcom LIKE ? OR m_gedcom LIKE ?)"; $vars[] = "%3 TYPE {$type}%"; $vars[] = "%1 _TYPE {$type}%"; } } return (int) Database::prepare($sql)->execute($vars)->fetchOne(); } /** * Count the number of media records. * * @return string */ public function totalMedia() { return I18N::number($this->totalMediaType('all')); } /** * Count the number of media records with type "audio". * * @return string */ public function totalMediaAudio() { return I18N::number($this->totalMediaType('audio')); } /** * Count the number of media records with type "book". * * @return string */ public function totalMediaBook() { return I18N::number($this->totalMediaType('book')); } /** * Count the number of media records with type "card". * * @return string */ public function totalMediaCard() { return I18N::number($this->totalMediaType('card')); } /** * Count the number of media records with type "certificate". * * @return string */ public function totalMediaCertificate() { return I18N::number($this->totalMediaType('certificate')); } /** * Count the number of media records with type "coat of arms". * * @return string */ public function totalMediaCoatOfArms() { return I18N::number($this->totalMediaType('coat')); } /** * Count the number of media records with type "document". * * @return string */ public function totalMediaDocument() { return I18N::number($this->totalMediaType('document')); } /** * Count the number of media records with type "electronic". * * @return string */ public function totalMediaElectronic() { return I18N::number($this->totalMediaType('electronic')); } /** * Count the number of media records with type "magazine". * * @return string */ public function totalMediaMagazine() { return I18N::number($this->totalMediaType('magazine')); } /** * Count the number of media records with type "manuscript". * * @return string */ public function totalMediaManuscript() { return I18N::number($this->totalMediaType('manuscript')); } /** * Count the number of media records with type "map". * * @return string */ public function totalMediaMap() { return I18N::number($this->totalMediaType('map')); } /** * Count the number of media records with type "microfiche". * * @return string */ public function totalMediaFiche() { return I18N::number($this->totalMediaType('fiche')); } /** * Count the number of media records with type "microfilm". * * @return string */ public function totalMediaFilm() { return I18N::number($this->totalMediaType('film')); } /** * Count the number of media records with type "newspaper". * * @return string */ public function totalMediaNewspaper() { return I18N::number($this->totalMediaType('newspaper')); } /** * Count the number of media records with type "painting". * * @return string */ public function totalMediaPainting() { return I18N::number($this->totalMediaType('painting')); } /** * Count the number of media records with type "photograph". * * @return string */ public function totalMediaPhoto() { return I18N::number($this->totalMediaType('photo')); } /** * Count the number of media records with type "tombstone". * * @return string */ public function totalMediaTombstone() { return I18N::number($this->totalMediaType('tombstone')); } /** * Count the number of media records with type "video". * * @return string */ public function totalMediaVideo() { return I18N::number($this->totalMediaType('video')); } /** * Count the number of media records with type "other". * * @return string */ public function totalMediaOther() { return I18N::number($this->totalMediaType('other')); } /** * Count the number of media records with type "unknown". * * @return string */ public function totalMediaUnknown() { return I18N::number($this->totalMediaType('unknown')); } /** * Create a chart of media types. * * @param string[] $params * * @return string */ public function chartMedia($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } $sizes = explode('x', $size); $tot = $this->totalMediaType('all'); // Beware divide by zero if ($tot == 0) { return I18N::translate('None'); } // Build a table listing only the media types actually present in the GEDCOM $mediaCounts = array(); $mediaTypes = ""; $chart_title = ""; $c = 0; $max = 0; $media = array(); foreach ($this->_media_types as $type) { $count = $this->totalMediaType($type); if ($count > 0) { $media[$type] = $count; if ($count > $max) { $max = $count; } $c += $count; } } $count = $this->totalMediaType('unknown'); if ($count > 0) { $media['unknown'] = $tot - $c; if ($tot - $c > $max) { $max = $count; } } if (($max / $tot) > 0.6 && count($media) > 10) { arsort($media); $media = array_slice($media, 0, 10); $c = $tot; foreach ($media as $cm) { $c -= $cm; } if (isset($media['other'])) { $media['other'] += $c; } else { $media['other'] = $c; } } asort($media); foreach ($media as $type => $count) { $mediaCounts[] = round(100 * $count / $tot, 0); $mediaTypes .= GedcomTag::getFileFormTypeValue($type) . ' - ' . I18N::number($count) . '|'; $chart_title .= GedcomTag::getFileFormTypeValue($type) . ' (' . $count . '), '; } $chart_title = substr($chart_title, 0, -2); $chd = $this->arrayToExtendedEncoding($mediaCounts); $chl = substr($mediaTypes, 0, -1); return "\"""; } /** * Birth and Death * * @param string $type * @param string $life_dir * @param string $birth_death * * @return string */ private function mortalityQuery($type = 'full', $life_dir = 'ASC', $birth_death = 'BIRT') { if ($birth_death == 'MARR') { $query_field = "'MARR'"; } elseif ($birth_death == 'DIV') { $query_field = "'DIV'"; } elseif ($birth_death == 'BIRT') { $query_field = "'BIRT'"; } else { $query_field = "'DEAT'"; } if ($life_dir == 'ASC') { $dmod = 'MIN'; } else { $dmod = 'MAX'; } $rows = $this->runSql( "SELECT SQL_CACHE d_year, d_type, d_fact, d_gid" . " FROM `##dates`" . " WHERE d_file={$this->tree->getTreeId()} AND d_fact IN ({$query_field}) AND d_julianday1=(" . " SELECT {$dmod}( d_julianday1 )" . " FROM `##dates`" . " WHERE d_file={$this->tree->getTreeId()} AND d_fact IN ({$query_field}) AND d_julianday1<>0 )" . " LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; $record = GedcomRecord::getInstance($row['d_gid'], $this->tree); switch ($type) { default: case 'full': if ($record->canShow()) { $result = $record->formatList('span', false, $record->getFullName()); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'year': $date = new Date($row['d_type'] . ' ' . $row['d_year']); $result = $date->display(); break; case 'name': $result = "getHtmlUrl() . "\">" . $record->getFullName() . ""; break; case 'place': $fact = GedcomRecord::getInstance($row['d_gid'], $this->tree)->getFirstFact($row['d_fact']); if ($fact) { $result = FunctionsPrint::formatFactPlace($fact, true, true, true); } else { $result = I18N::translate('Private'); } break; } return $result; } /** * Places * * @param string $what * @param string $fact * @param int $parent * @param bool $country * * @return int[]|string[][] */ public function statsPlaces($what = 'ALL', $fact = '', $parent = 0, $country = false) { if ($fact) { if ($what == 'INDI') { $rows = Database::prepare( "SELECT i_gedcom AS ged FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom LIKE '%\n2 PLAC %'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchAll(); } elseif ($what == 'FAM') { $rows = Database::prepare( "SELECT f_gedcom AS ged FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\n2 PLAC %'" )->execute(array( 'tree_id' => $this->tree->getTreeId(), ))->fetchAll(); } $placelist = array(); foreach ($rows as $row) { if (preg_match('/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $row->ged, $match)) { if ($country) { $tmp = explode(Place::GEDCOM_SEPARATOR, $match[1]); $place = end($tmp); } else { $place = $match[1]; } if (!isset($placelist[$place])) { $placelist[$place] = 1; } else { $placelist[$place]++; } } } return $placelist; } elseif ($parent > 0) { // used by placehierarchy googlemap module if ($what == 'INDI') { $join = " JOIN `##individuals` ON pl_file = i_file AND pl_gid = i_id"; } elseif ($what == 'FAM') { $join = " JOIN `##families` ON pl_file = f_file AND pl_gid = f_id"; } else { $join = ""; } $rows = $this->runSql( " SELECT SQL_CACHE" . " p_place AS place," . " COUNT(*) AS tot" . " FROM" . " `##places`" . " JOIN `##placelinks` ON pl_file=p_file AND p_id=pl_p_id" . $join . " WHERE" . " p_id={$parent} AND" . " p_file={$this->tree->getTreeId()}" . " GROUP BY place" ); return $rows; } else { if ($what == 'INDI') { $join = " JOIN `##individuals` ON pl_file = i_file AND pl_gid = i_id"; } elseif ($what == 'FAM') { $join = " JOIN `##families` ON pl_file = f_file AND pl_gid = f_id"; } else { $join = ""; } $rows = $this->runSql( " SELECT SQL_CACHE" . " p_place AS country," . " COUNT(*) AS tot" . " FROM" . " `##places`" . " JOIN `##placelinks` ON pl_file=p_file AND p_id=pl_p_id" . $join . " WHERE" . " p_file={$this->tree->getTreeId()}" . " AND p_parent_id='0'" . " GROUP BY country ORDER BY tot DESC, country ASC" ); return $rows; } } /** * Count total places. * * @return int */ private function totalPlacesQuery() { return (int) Database::prepare("SELECT SQL_CACHE COUNT(*) FROM `##places` WHERE p_file=?") ->execute(array($this->tree->getTreeId())) ->fetchOne(); } /** * Count total places. * * @return string */ public function totalPlaces() { return I18N::number($this->totalPlacesQuery()); } /** * Create a chart showing where events occurred. * * @param string[] $params * * @return string */ public function chartDistribution($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_CHART_COLOR3 = Theme::theme()->parameter('distribution-chart-low-values'); $WT_STATS_MAP_X = Theme::theme()->parameter('distribution-chart-x'); $WT_STATS_MAP_Y = Theme::theme()->parameter('distribution-chart-y'); if (isset($params[0])) { $chart_shows = $params[0]; } else { $chart_shows = 'world'; } if (isset($params[1])) { $chart_type = $params[1]; } else { $chart_type = ''; } if (isset($params[2])) { $surname = $params[2]; } else { $surname = ''; } if ($this->totalPlacesQuery() == 0) { return ''; } // Get the country names for each language $country_to_iso3166 = array(); foreach (I18N::activeLocales() as $locale) { I18N::init($locale->languageTag()); $countries = $this->getAllCountries(); foreach ($this->iso3166() as $three => $two) { $country_to_iso3166[$three] = $two; $country_to_iso3166[$countries[$three]] = $two; } } I18N::init(WT_LOCALE); switch ($chart_type) { case 'surname_distribution_chart': if ($surname == "") { $surname = $this->getCommonSurname(); } $chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname; // Count how many people are events in each country $surn_countries = array(); $indis = QueryName::individuals($this->tree, I18N::strtoupper($surname), '', '', false, false); foreach ($indis as $person) { if (preg_match_all('/^2 PLAC (?:.*, *)*(.*)/m', $person->getGedcom(), $matches)) { // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes. foreach ($matches[1] as $country) { if (array_key_exists($country, $country_to_iso3166)) { if (array_key_exists($country_to_iso3166[$country], $surn_countries)) { $surn_countries[$country_to_iso3166[$country]]++; } else { $surn_countries[$country_to_iso3166[$country]] = 1; } } } } }; break; case 'birth_distribution_chart': $chart_title = I18N::translate('Birth by country'); // Count how many people were born in each country $surn_countries = array(); $b_countries = $this->statsPlaces('INDI', 'BIRT', 0, true); foreach ($b_countries as $place => $count) { $country = $place; if (array_key_exists($country, $country_to_iso3166)) { if (!isset($surn_countries[$country_to_iso3166[$country]])) { $surn_countries[$country_to_iso3166[$country]] = $count; } else { $surn_countries[$country_to_iso3166[$country]] += $count; } } } break; case 'death_distribution_chart': $chart_title = I18N::translate('Death by country'); // Count how many people were death in each country $surn_countries = array(); $d_countries = $this->statsPlaces('INDI', 'DEAT', 0, true); foreach ($d_countries as $place => $count) { $country = $place; if (array_key_exists($country, $country_to_iso3166)) { if (!isset($surn_countries[$country_to_iso3166[$country]])) { $surn_countries[$country_to_iso3166[$country]] = $count; } else { $surn_countries[$country_to_iso3166[$country]] += $count; } } } break; case 'marriage_distribution_chart': $chart_title = I18N::translate('Marriage by country'); // Count how many families got marriage in each country $surn_countries = array(); $m_countries = $this->statsPlaces('FAM'); // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes. foreach ($m_countries as $place) { $country = $place['country']; if (array_key_exists($country, $country_to_iso3166)) { if (!isset($surn_countries[$country_to_iso3166[$country]])) { $surn_countries[$country_to_iso3166[$country]] = $place['tot']; } else { $surn_countries[$country_to_iso3166[$country]] += $place['tot']; } } } break; case 'indi_distribution_chart': default: $chart_title = I18N::translate('Individual distribution chart'); // Count how many people have events in each country $surn_countries = array(); $a_countries = $this->statsPlaces('INDI'); // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes. foreach ($a_countries as $place) { $country = $place['country']; if (array_key_exists($country, $country_to_iso3166)) { if (!isset($surn_countries[$country_to_iso3166[$country]])) { $surn_countries[$country_to_iso3166[$country]] = $place['tot']; } else { $surn_countries[$country_to_iso3166[$country]] += $place['tot']; } } } break; } $chart_url = "https://chart.googleapis.com/chart?cht=t&chtm=" . $chart_shows; $chart_url .= "&chco=" . $WT_STATS_CHART_COLOR1 . "," . $WT_STATS_CHART_COLOR3 . "," . $WT_STATS_CHART_COLOR2; // country colours $chart_url .= "&chf=bg,s,ECF5FF"; // sea colour $chart_url .= "&chs=" . $WT_STATS_MAP_X . "x" . $WT_STATS_MAP_Y; $chart_url .= "&chld=" . implode('', array_keys($surn_countries)) . "&chd=s:"; foreach ($surn_countries as $count) { $chart_url .= substr(WT_GOOGLE_CHART_ENCODING, (int) ($count / max($surn_countries) * 61), 1); } $chart = '
'; $chart .= '

' . $chart_title . '

'; $chart .= '
' . $chart_title . '
'; $chart .= ''; $chart .= ''; $chart .= ''; $chart .= ''; $chart .= '
' . I18N::translate('Highest population') . '' . I18N::translate('Lowest population') . '' . I18N::translate('Nobody at all') . '
'; return $chart; } /** * A list of common countries. * * @return string */ public function commonCountriesList() { $countries = $this->statsPlaces(); if (empty($countries)) { return ''; } $top10 = array(); $i = 1; // Get the country names for each language $country_names = array(); foreach (I18N::activeLocales() as $locale) { I18N::init($locale->languageTag()); $all_countries = $this->getAllCountries(); foreach ($all_countries as $country_code => $country_name) { $country_names[$country_name] = $country_code; } } I18N::init(WT_LOCALE); $all_db_countries = array(); foreach ($countries as $place) { $country = trim($place['country']); if (array_key_exists($country, $country_names)) { if (!isset($all_db_countries[$country_names[$country]][$country])) { $all_db_countries[$country_names[$country]][$country] = (int) $place['tot']; } else { $all_db_countries[$country_names[$country]][$country] += (int) $place['tot']; } } } // get all the user’s countries names $all_countries = $this->getAllCountries(); foreach ($all_db_countries as $country_code => $country) { $top10[] = '
  • '; foreach ($country as $country_name => $tot) { $tmp = new Place($country_name, $this->tree); $place = '' . $all_countries[$country_code] . ''; $top10[] .= $place . ' - ' . I18N::number($tot); } $top10[] .= '
  • '; if ($i++ == 10) { break; } } $top10 = implode('', $top10); return ''; } /** * A list of common birth places. * * @return string */ public function commonBirthPlacesList() { $places = $this->statsPlaces('INDI', 'BIRT'); $top10 = array(); $i = 1; arsort($places); foreach ($places as $place => $count) { $tmp = new Place($place, $this->tree); $place = '' . $tmp->getFullName() . ''; $top10[] = '
  • ' . $place . ' - ' . I18N::number($count) . '
  • '; if ($i++ == 10) { break; } } $top10 = implode('', $top10); return ''; } /** * A list of common death places. * * @return string */ public function commonDeathPlacesList() { $places = $this->statsPlaces('INDI', 'DEAT'); $top10 = array(); $i = 1; arsort($places); foreach ($places as $place => $count) { $tmp = new Place($place, $this->tree); $place = '' . $tmp->getFullName() . ''; $top10[] = '
  • ' . $place . ' - ' . I18N::number($count) . '
  • '; if ($i++ == 10) { break; } } $top10 = implode('', $top10); return ''; } /** * A list of common marriage places. * * @return string */ public function commonMarriagePlacesList() { $places = $this->statsPlaces('FAM', 'MARR'); $top10 = array(); $i = 1; arsort($places); foreach ($places as $place => $count) { $tmp = new Place($place, $this->tree); $place = '' . $tmp->getFullName() . ''; $top10[] = '
  • ' . $place . ' - ' . I18N::number($count) . '
  • '; if ($i++ == 10) { break; } } $top10 = implode('', $top10); return ''; } /** * Create a chart of birth places. * * @param bool $simple * @param bool $sex * @param int $year1 * @param int $year2 * @param string[] $params * * @return array|string */ public function statsBirthQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if ($simple) { $sql = "SELECT SQL_CACHE FLOOR(d_year/100+1) AS century, COUNT(*) AS total FROM `##dates` " . "WHERE " . "d_file={$this->tree->getTreeId()} AND " . "d_year<>0 AND " . "d_fact='BIRT' AND " . "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; } elseif ($sex) { $sql = "SELECT SQL_CACHE d_month, i_sex, COUNT(*) AS total FROM `##dates` " . "JOIN `##individuals` ON d_file = i_file AND d_gid = i_id " . "WHERE " . "d_file={$this->tree->getTreeId()} AND " . "d_fact='BIRT' AND " . "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; } else { $sql = "SELECT SQL_CACHE d_month, COUNT(*) AS total FROM `##dates` " . "WHERE " . "d_file={$this->tree->getTreeId()} AND " . "d_fact='BIRT' AND " . "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; } if ($year1 >= 0 && $year2 >= 0) { $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; } if ($simple) { $sql .= " GROUP BY century ORDER BY century"; } else { $sql .= " GROUP BY d_month"; if ($sex) { $sql .= ", i_sex"; } } $rows = $this->runSql($sql); if ($simple) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } $sizes = explode('x', $size); $tot = 0; foreach ($rows as $values) { $tot += $values['total']; } // Beware divide by zero if ($tot == 0) { return ''; } $centuries = ""; $counts = array(); foreach ($rows as $values) { $counts[] = round(100 * $values['total'] / $tot, 0); $centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|'; } $chd = $this->arrayToExtendedEncoding($counts); $chl = rawurlencode(substr($centuries, 0, -1)); return "\"""; } else { return $rows; } } /** * Create a chart of death places. * * @param bool $simple * @param bool $sex * @param int $year1 * @param int $year2 * @param string[] $params * * @return array|string */ public function statsDeathQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if ($simple) { $sql = "SELECT SQL_CACHE FLOOR(d_year/100+1) AS century, COUNT(*) AS total FROM `##dates` " . "WHERE " . "d_file={$this->tree->getTreeId()} AND " . 'd_year<>0 AND ' . "d_fact='DEAT' AND " . "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; } elseif ($sex) { $sql = "SELECT SQL_CACHE d_month, i_sex, COUNT(*) AS total FROM `##dates` " . "JOIN `##individuals` ON d_file = i_file AND d_gid = i_id " . "WHERE " . "d_file={$this->tree->getTreeId()} AND " . "d_fact='DEAT' AND " . "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; } else { $sql = "SELECT SQL_CACHE d_month, COUNT(*) AS total FROM `##dates` " . "WHERE " . "d_file={$this->tree->getTreeId()} AND " . "d_fact='DEAT' AND " . "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; } if ($year1 >= 0 && $year2 >= 0) { $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; } if ($simple) { $sql .= " GROUP BY century ORDER BY century"; } else { $sql .= " GROUP BY d_month"; if ($sex) { $sql .= ", i_sex"; } } $rows = $this->runSql($sql); if ($simple) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } $sizes = explode('x', $size); $tot = 0; foreach ($rows as $values) { $tot += $values['total']; } // Beware divide by zero if ($tot == 0) { return ''; } $centuries = ""; $counts = array(); foreach ($rows as $values) { $counts[] = round(100 * $values['total'] / $tot, 0); $centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|'; } $chd = $this->arrayToExtendedEncoding($counts); $chl = rawurlencode(substr($centuries, 0, -1)); return "\"""; } return $rows; } /** * Find the earliest birth. * * @return string */ public function firstBirth() { return $this->mortalityQuery('full', 'ASC', 'BIRT'); } /** * Find the earliest birth year. * * @return string */ public function firstBirthYear() { return $this->mortalityQuery('year', 'ASC', 'BIRT'); } /** * Find the name of the earliest birth. * * @return string */ public function firstBirthName() { return $this->mortalityQuery('name', 'ASC', 'BIRT'); } /** * Find the earliest birth place. * * @return string */ public function firstBirthPlace() { return $this->mortalityQuery('place', 'ASC', 'BIRT'); } /** * Find the latest birth. * * @return string */ public function lastBirth() { return $this->mortalityQuery('full', 'DESC', 'BIRT'); } /** * Find the latest birth year. * * @return string */ public function lastBirthYear() { return $this->mortalityQuery('year', 'DESC', 'BIRT'); } /** * Find the latest birth name. * * @return string */ public function lastBirthName() { return $this->mortalityQuery('name', 'DESC', 'BIRT'); } /** * Find the latest birth place. * * @return string */ public function lastBirthPlace() { return $this->mortalityQuery('place', 'DESC', 'BIRT'); } /** * General query on births. * * @param string[] $params * * @return string */ public function statsBirth($params = array()) { return $this->statsBirthQuery(true, false, -1, -1, $params); } /** * Find the earliest death. * * @return string */ public function firstDeath() { return $this->mortalityQuery('full', 'ASC', 'DEAT'); } /** * Find the earliest death year. * * @return string */ public function firstDeathYear() { return $this->mortalityQuery('year', 'ASC', 'DEAT'); } /** * Find the earliest death name. * * @return string */ public function firstDeathName() { return $this->mortalityQuery('name', 'ASC', 'DEAT'); } /** * Find the earliest death place. * * @return string */ public function firstDeathPlace() { return $this->mortalityQuery('place', 'ASC', 'DEAT'); } /** * Find the latest death. * * @return string */ public function lastDeath() { return $this->mortalityQuery('full', 'DESC', 'DEAT'); } /** * Find the latest death year. * * @return string */ public function lastDeathYear() { return $this->mortalityQuery('year', 'DESC', 'DEAT'); } /** * Find the latest death name. * * @return string */ public function lastDeathName() { return $this->mortalityQuery('name', 'DESC', 'DEAT'); } /** * Find the place of the latest death. * * @return string */ public function lastDeathPlace() { return $this->mortalityQuery('place', 'DESC', 'DEAT'); } /** * General query on deaths. * * @param string[] $params * * @return string */ public function statsDeath($params = array()) { return $this->statsDeathQuery(true, false, -1, -1, $params); } /** * Lifespan * * @param string $type * @param string $sex * * @return string */ private function longlifeQuery($type = 'full', $sex = 'F') { $sex_search = ' 1=1'; if ($sex == 'F') { $sex_search = " i_sex='F'"; } elseif ($sex == 'M') { $sex_search = " i_sex='M'"; } $rows = $this->runSql( " SELECT SQL_CACHE" . " death.d_gid AS id," . " death.d_julianday2-birth.d_julianday1 AS age" . " FROM" . " `##dates` AS death," . " `##dates` AS birth," . " `##individuals` AS indi" . " WHERE" . " indi.i_id=birth.d_gid AND" . " birth.d_gid=death.d_gid AND" . " death.d_file={$this->tree->getTreeId()} AND" . " birth.d_file=death.d_file AND" . " birth.d_file=indi.i_file AND" . " birth.d_fact='BIRT' AND" . " death.d_fact='DEAT' AND" . " birth.d_julianday1<>0 AND" . " death.d_julianday1>birth.d_julianday2 AND" . $sex_search . " ORDER BY" . " age DESC LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; $person = Individual::getInstance($row['id'], $this->tree); switch ($type) { default: case 'full': if ($person->canShowName()) { $result = $person->formatList('span', false, $person->getFullName()); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'age': $result = I18N::number((int) ($row['age'] / 365.25)); break; case 'name': $result = "getHtmlUrl() . "\">" . $person->getFullName() . ""; break; } return $result; } /** * Find the oldest individuals. * * @param string $type * @param string $sex * @param string[] $params * * @return string */ private function topTenOldestQuery($type = 'list', $sex = 'BOTH', $params = array()) { if ($sex === 'F') { $sex_search = " AND i_sex='F' "; } elseif ($sex === 'M') { $sex_search = " AND i_sex='M' "; } else { $sex_search = ''; } if (isset($params[0])) { $total = (int) $params[0]; } else { $total = 10; } $rows = $this->runSql( "SELECT SQL_CACHE " . " MAX(death.d_julianday2-birth.d_julianday1) AS age, " . " death.d_gid AS deathdate " . "FROM " . " `##dates` AS death, " . " `##dates` AS birth, " . " `##individuals` AS indi " . "WHERE " . " indi.i_id=birth.d_gid AND " . " birth.d_gid=death.d_gid AND " . " death.d_file={$this->tree->getTreeId()} AND " . " birth.d_file=death.d_file AND " . " birth.d_file=indi.i_file AND " . " birth.d_fact='BIRT' AND " . " death.d_fact='DEAT' AND " . " birth.d_julianday1<>0 AND " . " death.d_julianday1>birth.d_julianday2 " . $sex_search . "GROUP BY deathdate " . "ORDER BY age DESC " . "LIMIT " . $total ); if (!isset($rows[0])) { return ''; } $top10 = array(); foreach ($rows as $row) { $person = Individual::getInstance($row['deathdate'], $this->tree); $age = $row['age']; if ((int) ($age / 365.25) > 0) { $age = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $age = (int) ($age / 30.4375) . 'm'; } else { $age = $age . 'd'; } $age = FunctionsDate::getAgeAtEvent($age); if ($person->canShow()) { if ($type == 'list') { $top10[] = "
  • getHtmlUrl() . "\">" . $person->getFullName() . " (" . $age . ")" . "
  • "; } else { $top10[] = "getHtmlUrl() . "\">" . $person->getFullName() . " (" . $age . ")"; } } } if ($type == 'list') { $top10 = implode('', $top10); } else { $top10 = implode(' ', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace(array('[', ']', '(', ')', '+'), array('‏[', '‏]', '‏(', '‏)', '‏+'), $top10); } if ($type == 'list') { return ''; } return $top10; } /** * Find the oldest living individuals. * * @param string $type * @param string $sex * @param string[] $params * * @return string */ private function topTenOldestAliveQuery($type = 'list', $sex = 'BOTH', $params = array()) { if (!Auth::isMember($this->tree)) { return I18N::translate('This information is private and cannot be shown.'); } if ($sex == 'F') { $sex_search = " AND i_sex='F'"; } elseif ($sex == 'M') { $sex_search = " AND i_sex='M'"; } else { $sex_search = ''; } if (isset($params[0])) { $total = (int) $params[0]; } else { $total = 10; } $rows = $this->runSql( "SELECT SQL_CACHE" . " birth.d_gid AS id," . " MIN(birth.d_julianday1) AS age" . " FROM" . " `##dates` AS birth," . " `##individuals` AS indi" . " WHERE" . " indi.i_id=birth.d_gid AND" . " indi.i_gedcom NOT REGEXP '\\n1 (" . WT_EVENTS_DEAT . ")' AND" . " birth.d_file={$this->tree->getTreeId()} AND" . " birth.d_fact='BIRT' AND" . " birth.d_file=indi.i_file AND" . " birth.d_julianday1<>0" . $sex_search . " GROUP BY id" . " ORDER BY age" . " ASC LIMIT " . $total ); $top10 = array(); foreach ($rows as $row) { $person = Individual::getInstance($row['id'], $this->tree); $age = (WT_CLIENT_JD - $row['age']); if ((int) ($age / 365.25) > 0) { $age = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $age = (int) ($age / 30.4375) . 'm'; } else { $age = $age . 'd'; } $age = FunctionsDate::getAgeAtEvent($age); if ($type === 'list') { $top10[] = "
  • getHtmlUrl() . "\">" . $person->getFullName() . " (" . $age . ")" . "
  • "; } else { $top10[] = "getHtmlUrl() . "\">" . $person->getFullName() . " (" . $age . ")"; } } if ($type === 'list') { $top10 = implode('', $top10); } else { $top10 = implode('; ', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace(array('[', ']', '(', ')', '+'), array('‏[', '‏]', '‏(', '‏)', '‏+'), $top10); } if ($type === 'list') { return ''; } return $top10; } /** * Find the average lifespan. * * @param string $sex * @param bool $show_years * * @return string */ private function averageLifespanQuery($sex = 'BOTH', $show_years = false) { if ($sex === 'F') { $sex_search = " AND i_sex='F' "; } elseif ($sex === 'M') { $sex_search = " AND i_sex='M' "; } else { $sex_search = ''; } $rows = $this->runSql( "SELECT SQL_CACHE " . " AVG(death.d_julianday2-birth.d_julianday1) AS age " . "FROM " . " `##dates` AS death, " . " `##dates` AS birth, " . " `##individuals` AS indi " . "WHERE " . " indi.i_id=birth.d_gid AND " . " birth.d_gid=death.d_gid AND " . " death.d_file=" . $this->tree->getTreeId() . " AND " . " birth.d_file=death.d_file AND " . " birth.d_file=indi.i_file AND " . " birth.d_fact='BIRT' AND " . " death.d_fact='DEAT' AND " . " birth.d_julianday1<>0 AND " . " death.d_julianday1>birth.d_julianday2 " . $sex_search ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; $age = $row['age']; if ($show_years) { if ((int) ($age / 365.25) > 0) { $age = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $age = (int) ($age / 30.4375) . 'm'; } elseif (!empty($age)) { $age = $age . 'd'; } return FunctionsDate::getAgeAtEvent($age); } else { return I18N::number($age / 365.25); } } /** * General query on ages. * * @param bool $simple * @param string $related * @param string $sex * @param int $year1 * @param int $year2 * @param string[] $params * * @return array|string */ public function statsAgeQuery($simple = true, $related = 'BIRT', $sex = 'BOTH', $year1 = -1, $year2 = -1, $params = array()) { if ($simple) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = '230x250'; } $sizes = explode('x', $size); $rows = $this->runSql( "SELECT SQL_CACHE" . " ROUND(AVG(death.d_julianday2-birth.d_julianday1)/365.25,1) AS age," . " FLOOR(death.d_year/100+1) AS century," . " i_sex AS sex" . " FROM" . " `##dates` AS death," . " `##dates` AS birth," . " `##individuals` AS indi" . " WHERE" . " indi.i_id=birth.d_gid AND" . " birth.d_gid=death.d_gid AND" . " death.d_file={$this->tree->getTreeId()} AND" . " birth.d_file=death.d_file AND" . " birth.d_file=indi.i_file AND" . " birth.d_fact='BIRT' AND" . " death.d_fact='DEAT' AND" . " birth.d_julianday1<>0 AND" . " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" . " death.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" . " death.d_julianday1>birth.d_julianday2" . " GROUP BY century, sex ORDER BY century, sex"); if (empty($rows)) { return ''; } $chxl = '0:|'; $countsm = ''; $countsf = ''; $countsa = ''; $out = array(); foreach ($rows as $values) { $out[$values['century']][$values['sex']] = $values['age']; } foreach ($out as $century => $values) { if ($sizes[0] < 980) { $sizes[0] += 50; } $chxl .= $this->centuryName($century) . '|'; $average = 0; if (isset($values['F'])) { $countsf .= $values['F'] . ','; $average = $values['F']; } else { $countsf .= '0,'; } if (isset($values['M'])) { $countsm .= $values['M'] . ','; if ($average == 0) { $countsa .= $values['M'] . ','; } else { $countsa .= (($values['M'] + $average) / 2) . ','; } } else { $countsm .= '0,'; if ($average == 0) { $countsa .= '0,'; } else { $countsa .= $values['F'] . ','; } } } $countsm = substr($countsm, 0, -1); $countsf = substr($countsf, 0, -1); $countsa = substr($countsa, 0, -1); $chd = 't2:' . $countsm . '|' . $countsf . '|' . $countsa; $decades = ''; for ($i = 0; $i <= 100; $i += 10) { $decades .= '|' . I18N::number($i); } $chxl .= '1:||' . I18N::translate('century') . '|2:' . $decades . '|3:||' . I18N::translate('Age') . '|'; $title = I18N::translate('Average age related to death century'); if (count($rows) > 6 || mb_strlen($title) < 30) { $chtt = $title; } else { $offset = 0; $counter = array(); while ($offset = strpos($title, ' ', $offset + 1)) { $counter[] = $offset; } $half = (int) (count($counter) / 2); $chtt = substr_replace($title, '|', $counter[$half], 1); } return '\"""; } else { $sex_search = ''; $years = ''; if ($sex == 'F') { $sex_search = " AND i_sex='F'"; } elseif ($sex == 'M') { $sex_search = " AND i_sex='M'"; } if ($year1 >= 0 && $year2 >= 0) { if ($related == 'BIRT') { $years = " AND birth.d_year BETWEEN '{$year1}' AND '{$year2}'"; } elseif ($related == 'DEAT') { $years = " AND death.d_year BETWEEN '{$year1}' AND '{$year2}'"; } } $rows = $this->runSql( "SELECT SQL_CACHE" . " death.d_julianday2-birth.d_julianday1 AS age" . " FROM" . " `##dates` AS death," . " `##dates` AS birth," . " `##individuals` AS indi" . " WHERE" . " indi.i_id=birth.d_gid AND" . " birth.d_gid=death.d_gid AND" . " death.d_file={$this->tree->getTreeId()} AND" . " birth.d_file=death.d_file AND" . " birth.d_file=indi.i_file AND" . " birth.d_fact='BIRT' AND" . " death.d_fact='DEAT' AND" . " birth.d_julianday1<>0 AND" . " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" . " death.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" . " death.d_julianday1>birth.d_julianday2" . $years . $sex_search . " ORDER BY age DESC"); return $rows; } } /** * General query on ages. * * @param string[] $params * * @return string */ public function statsAge($params = array()) { return $this->statsAgeQuery(true, 'BIRT', 'BOTH', -1, -1, $params); } /** * Find the lognest lived individual. * * @return string */ public function longestLife() { return $this->longlifeQuery('full', 'BOTH'); } /** * Find the age of the longest lived individual. * * @return string */ public function longestLifeAge() { return $this->longlifeQuery('age', 'BOTH'); } /** * Find the name of the longest lived individual. * * @return string */ public function longestLifeName() { return $this->longlifeQuery('name', 'BOTH'); } /** * Find the oldest individuals. * * @param string[] $params * * @return string */ public function topTenOldest($params = array()) { return $this->topTenOldestQuery('nolist', 'BOTH', $params); } /** * Find the oldest living individuals. * * @param string[] $params * * @return string */ public function topTenOldestList($params = array()) { return $this->topTenOldestQuery('list', 'BOTH', $params); } /** * Find the oldest living individuals. * * @param string[] $params * * @return string */ public function topTenOldestAlive($params = array()) { return $this->topTenOldestAliveQuery('nolist', 'BOTH', $params); } /** * Find the oldest living individuals. * * @param string[] $params * * @return string */ public function topTenOldestListAlive($params = array()) { return $this->topTenOldestAliveQuery('list', 'BOTH', $params); } /** * Find the average lifespan. * * @param bool $show_years * * @return string */ public function averageLifespan($show_years = false) { return $this->averageLifespanQuery('BOTH', $show_years); } /** * Find the longest lived female. * * @return string */ public function longestLifeFemale() { return $this->longlifeQuery('full', 'F'); } /** * Find the age of the longest lived female. * * @return string */ public function longestLifeFemaleAge() { return $this->longlifeQuery('age', 'F'); } /** * Find the name of the longest lived female. * * @return string */ public function longestLifeFemaleName() { return $this->longlifeQuery('name', 'F'); } /** * Find the oldest females. * * @param string[] $params * * @return string */ public function topTenOldestFemale($params = array()) { return $this->topTenOldestQuery('nolist', 'F', $params); } /** * Find the oldest living females. * * @param string[] $params * * @return string */ public function topTenOldestFemaleList($params = array()) { return $this->topTenOldestQuery('list', 'F', $params); } /** * Find the oldest living females. * * @param string[] $params * * @return string */ public function topTenOldestFemaleAlive($params = array()) { return $this->topTenOldestAliveQuery('nolist', 'F', $params); } /** * Find the oldest living females. * * @param string[] $params * * @return string */ public function topTenOldestFemaleListAlive($params = array()) { return $this->topTenOldestAliveQuery('list', 'F', $params); } /** * Find the average lifespan of females. * * @param bool $show_years * * @return string */ public function averageLifespanFemale($show_years = false) { return $this->averageLifespanQuery('F', $show_years); } /** * Find the longest lived male. * * @return string */ public function longestLifeMale() { return $this->longlifeQuery('full', 'M'); } /** * Find the age of the longest lived male. * * @return string */ public function longestLifeMaleAge() { return $this->longlifeQuery('age', 'M'); } /** * Find the name of the longest lived male. * * @return string */ public function longestLifeMaleName() { return $this->longlifeQuery('name', 'M'); } /** * Find the longest lived males. * * @param string[] $params * * @return string */ public function topTenOldestMale($params = array()) { return $this->topTenOldestQuery('nolist', 'M', $params); } /** * Find the longest lived males. * * @param string[] $params * * @return string */ public function topTenOldestMaleList($params = array()) { return $this->topTenOldestQuery('list', 'M', $params); } /** * Find the longest lived living males. * * @param string[] $params * * @return string */ public function topTenOldestMaleAlive($params = array()) { return $this->topTenOldestAliveQuery('nolist', 'M', $params); } /** * Find the longest lived living males. * * @param string[] $params * * @return string */ public function topTenOldestMaleListAlive($params = array()) { return $this->topTenOldestAliveQuery('list', 'M', $params); } /** * Find the average male lifespan. * * @param bool $show_years * * @return string */ public function averageLifespanMale($show_years = false) { return $this->averageLifespanQuery('M', $show_years); } /** * Events * * @param string $type * @param string $direction * @param string $facts * * @return string */ private function eventQuery($type, $direction, $facts) { $eventTypes = array( 'BIRT' => I18N::translate('birth'), 'DEAT' => I18N::translate('death'), 'MARR' => I18N::translate('marriage'), 'ADOP' => I18N::translate('adoption'), 'BURI' => I18N::translate('burial'), 'CENS' => I18N::translate('census added'), ); $fact_query = "IN ('" . str_replace('|', "','", $facts) . "')"; if ($direction != 'ASC') { $direction = 'DESC'; } $rows = $this->runSql('' . ' SELECT SQL_CACHE' . ' d_gid AS id,' . ' d_year AS year,' . ' d_fact AS fact,' . ' d_type AS type' . ' FROM' . " `##dates`" . ' WHERE' . " d_file={$this->tree->getTreeId()} AND" . " d_gid<>'HEAD' AND" . " d_fact {$fact_query} AND" . ' d_julianday1<>0' . ' ORDER BY' . " d_julianday1 {$direction}, d_type LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; $record = GedcomRecord::getInstance($row['id'], $this->tree); switch ($type) { default: case 'full': if ($record->canShow()) { $result = $record->formatList('span', false, $record->getFullName()); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'year': $date = new Date($row['type'] . ' ' . $row['year']); $result = $date->display(); break; case 'type': if (isset($eventTypes[$row['fact']])) { $result = $eventTypes[$row['fact']]; } else { $result = GedcomTag::getLabel($row['fact']); } break; case 'name': $result = "getHtmlUrl() . "\">" . $record->getFullName() . ""; break; case 'place': $fact = $record->getFirstFact($row['fact']); if ($fact) { $result = FunctionsPrint::formatFactPlace($fact, true, true, true); } else { $result = I18N::translate('Private'); } break; } return $result; } /** * Find the earliest event. * * @return string */ public function firstEvent() { return $this->eventQuery('full', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Find the year of the earliest event. * * @return string */ public function firstEventYear() { return $this->eventQuery('year', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Find the type of the earliest event. * * @return string */ public function firstEventType() { return $this->eventQuery('type', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Find the name of the individual with the earliest event. * * @return string */ public function firstEventName() { return $this->eventQuery('name', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Find the location of the earliest event. * * @return string */ public function firstEventPlace() { return $this->eventQuery('place', 'ASC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Find the latest event. * * @return string */ public function lastEvent() { return $this->eventQuery('full', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Find the year of the latest event. * * @return string */ public function lastEventYear() { return $this->eventQuery('year', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Find the type of the latest event. * * @return string */ public function lastEventType() { return $this->eventQuery('type', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Find the name of the individual with the latest event. * * @return string */ public function lastEventName() { return $this->eventQuery('name', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * FInd the location of the latest event. * * @return string */ public function lastEventPlace() { return $this->eventQuery('place', 'DESC', WT_EVENTS_BIRT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV . '|' . WT_EVENTS_DEAT); } /** * Query the database for marriage tags. * * @param string $type * @param string $age_dir * @param string $sex * @param bool $show_years * * @return string */ private function marriageQuery($type = 'full', $age_dir = 'ASC', $sex = 'F', $show_years = false) { if ($sex == 'F') { $sex_field = 'f_wife'; } else { $sex_field = 'f_husb'; } if ($age_dir != 'ASC') { $age_dir = 'DESC'; } $rows = $this->runSql( " SELECT SQL_CACHE fam.f_id AS famid, fam.{$sex_field}, married.d_julianday2-birth.d_julianday1 AS age, indi.i_id AS i_id" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS birth ON birth.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" . " WHERE" . " birth.d_gid = indi.i_id AND" . " married.d_gid = fam.f_id AND" . " indi.i_id = fam.{$sex_field} AND" . " fam.f_file = {$this->tree->getTreeId()} AND" . " birth.d_fact = 'BIRT' AND" . " married.d_fact = 'MARR' AND" . " birth.d_julianday1 <> 0 AND" . " married.d_julianday2 > birth.d_julianday1 AND" . " i_sex='{$sex}'" . " ORDER BY" . " married.d_julianday2-birth.d_julianday1 {$age_dir} LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; if (isset($row['famid'])) { $family = Family::getInstance($row['famid'], $this->tree); } if (isset($row['i_id'])) { $person = Individual::getInstance($row['i_id'], $this->tree); } switch ($type) { default: case 'full': if ($family->canShow()) { $result = $family->formatList('span', false, $person->getFullName()); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'name': $result = '' . $person->getFullName() . ''; break; case 'age': $age = $row['age']; if ($show_years) { if ((int) ($age / 365.25) > 0) { $age = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $age = (int) ($age / 30.4375) . 'm'; } else { $age = $age . 'd'; } $result = FunctionsDate::getAgeAtEvent($age); } else { $result = I18N::number((int) ($age / 365.25)); } break; } return $result; } /** * General query on age at marriage. * * @param string $type * @param string $age_dir * @param string[] $params * * @return string */ private function ageOfMarriageQuery($type = 'list', $age_dir = 'ASC', $params = array()) { if (isset($params[0])) { $total = (int) $params[0]; } else { $total = 10; } if ($age_dir != 'ASC') { $age_dir = 'DESC'; } $hrows = $this->runSql( " SELECT SQL_CACHE DISTINCT fam.f_id AS family, MIN(husbdeath.d_julianday2-married.d_julianday1) AS age" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##dates` AS husbdeath ON husbdeath.d_file = {$this->tree->getTreeId()}" . " WHERE" . " fam.f_file = {$this->tree->getTreeId()} AND" . " husbdeath.d_gid = fam.f_husb AND" . " husbdeath.d_fact = 'DEAT' AND" . " married.d_gid = fam.f_id AND" . " married.d_fact = 'MARR' AND" . " married.d_julianday1 < husbdeath.d_julianday2 AND" . " married.d_julianday1 <> 0" . " GROUP BY family" . " ORDER BY age {$age_dir}"); $wrows = $this->runSql( " SELECT SQL_CACHE DISTINCT fam.f_id AS family, MIN(wifedeath.d_julianday2-married.d_julianday1) AS age" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##dates` AS wifedeath ON wifedeath.d_file = {$this->tree->getTreeId()}" . " WHERE" . " fam.f_file = {$this->tree->getTreeId()} AND" . " wifedeath.d_gid = fam.f_wife AND" . " wifedeath.d_fact = 'DEAT' AND" . " married.d_gid = fam.f_id AND" . " married.d_fact = 'MARR' AND" . " married.d_julianday1 < wifedeath.d_julianday2 AND" . " married.d_julianday1 <> 0" . " GROUP BY family" . " ORDER BY age {$age_dir}"); $drows = $this->runSql( " SELECT SQL_CACHE DISTINCT fam.f_id AS family, MIN(divorced.d_julianday2-married.d_julianday1) AS age" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->getTreeId()}" . " WHERE" . " fam.f_file = {$this->tree->getTreeId()} AND" . " married.d_gid = fam.f_id AND" . " married.d_fact = 'MARR' AND" . " divorced.d_gid = fam.f_id AND" . " divorced.d_fact IN ('DIV', 'ANUL', '_SEPR', '_DETS') AND" . " married.d_julianday1 < divorced.d_julianday2 AND" . " married.d_julianday1 <> 0" . " GROUP BY family" . " ORDER BY age {$age_dir}"); if (!isset($hrows) && !isset($wrows) && !isset($drows)) { return ''; } $rows = array(); foreach ($drows as $family) { $rows[$family['family']] = $family['age']; } foreach ($hrows as $family) { if (!isset($rows[$family['family']])) { $rows[$family['family']] = $family['age']; } } foreach ($wrows as $family) { if (!isset($rows[$family['family']])) { $rows[$family['family']] = $family['age']; } elseif ($rows[$family['family']] > $family['age']) { $rows[$family['family']] = $family['age']; } } if ($age_dir === 'DESC') { arsort($rows); } else { asort($rows); } $top10 = array(); $i = 0; foreach ($rows as $fam => $age) { $family = Family::getInstance($fam, $this->tree); if ($type === 'name') { return $family->formatList('span', false, $family->getFullName()); } if ((int) ($age / 365.25) > 0) { $age = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $age = (int) ($age / 30.4375) . 'm'; } else { $age = $age . 'd'; } $age = FunctionsDate::getAgeAtEvent($age); if ($type === 'age') { return $age; } $husb = $family->getHusband(); $wife = $family->getWife(); if ($husb && $wife && ($husb->getAllDeathDates() && $wife->getAllDeathDates() || !$husb->isDead() || !$wife->isDead())) { if ($family->canShow()) { if ($type === 'list') { $top10[] = "
  • getHtmlUrl() . "\">" . $family->getFullName() . " (" . $age . ")" . "
  • "; } else { $top10[] = "getHtmlUrl() . "\">" . $family->getFullName() . " (" . $age . ")"; } } if (++$i === $total) { break; } } } if ($type === 'list') { $top10 = implode('', $top10); } else { $top10 = implode('; ', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace(array('[', ']', '(', ')', '+'), array('‏[', '‏]', '‏(', '‏)', '‏+'), $top10); } if ($type === 'list') { return ''; } return $top10; } /** * Find the ages between spouses. * * @param string $type * @param string $age_dir * @param string[] $params * * @return string */ private function ageBetweenSpousesQuery($type = 'list', $age_dir = 'DESC', $params = array()) { if (isset($params[0])) { $total = (int) $params[0]; } else { $total = 10; } if ($age_dir === 'DESC') { $sql = "SELECT SQL_CACHE f_id AS xref, MIN(wife.d_julianday2-husb.d_julianday1) AS age" . " FROM `##families`" . " JOIN `##dates` AS wife ON wife.d_gid = f_wife AND wife.d_file = f_file" . " JOIN `##dates` AS husb ON husb.d_gid = f_husb AND husb.d_file = f_file" . " WHERE f_file = :tree_id" . " AND husb.d_fact = 'BIRT'" . " AND wife.d_fact = 'BIRT'" . " AND wife.d_julianday2 >= husb.d_julianday1 AND husb.d_julianday1 <> 0" . " GROUP BY xref" . " ORDER BY age DESC" . " LIMIT :limit"; } else { $sql = "SELECT SQL_CACHE f_id AS xref, MIN(husb.d_julianday2-wife.d_julianday1) AS age" . " FROM `##families`" . " JOIN `##dates` AS wife ON wife.d_gid = f_wife AND wife.d_file = f_file" . " JOIN `##dates` AS husb ON husb.d_gid = f_husb AND husb.d_file = f_file" . " WHERE f_file = :tree_id" . " AND husb.d_fact = 'BIRT'" . " AND wife.d_fact = 'BIRT'" . " AND husb.d_julianday2 >= wife.d_julianday1 AND wife.d_julianday1 <> 0" . " GROUP BY xref" . " ORDER BY age DESC" . " LIMIT :limit"; } $rows = Database::prepare( $sql )->execute(array( 'tree_id' => $this->tree->getTreeId(), 'limit' => $total, ))->fetchAll(); $top10 = array(); foreach ($rows as $fam) { $family = Family::getInstance($fam->xref, $this->tree); if ($fam->age < 0) { break; } $age = $fam->age; if ((int) ($age / 365.25) > 0) { $age = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $age = (int) ($age / 30.4375) . 'm'; } else { $age = $age . 'd'; } $age = FunctionsDate::getAgeAtEvent($age); if ($family->canShow()) { if ($type === 'list') { $top10[] = '
  • ' . $family->getFullName() . ' (' . $age . ')' . "
  • "; } else { $top10[] = '' . $family->getFullName() . ' (' . $age . ')'; } } } if ($type === 'list') { $top10 = implode('', $top10); if ($top10) { $top10 = ''; } } else { $top10 = implode(' ', $top10); } return $top10; } /** * General query on parents. * * @param string $type * @param string $age_dir * @param string $sex * @param bool $show_years * * @return string */ private function parentsQuery($type = 'full', $age_dir = 'ASC', $sex = 'F', $show_years = false) { if ($sex == 'F') { $sex_field = 'WIFE'; } else { $sex_field = 'HUSB'; } if ($age_dir != 'ASC') { $age_dir = 'DESC'; } $rows = $this->runSql( " SELECT SQL_CACHE" . " parentfamily.l_to AS id," . " childbirth.d_julianday2-birth.d_julianday1 AS age" . " FROM `##link` AS parentfamily" . " JOIN `##link` AS childfamily ON childfamily.l_file = {$this->tree->getTreeId()}" . " JOIN `##dates` AS birth ON birth.d_file = {$this->tree->getTreeId()}" . " JOIN `##dates` AS childbirth ON childbirth.d_file = {$this->tree->getTreeId()}" . " WHERE" . " birth.d_gid = parentfamily.l_to AND" . " childfamily.l_to = childbirth.d_gid AND" . " childfamily.l_type = 'CHIL' AND" . " parentfamily.l_type = '{$sex_field}' AND" . " childfamily.l_from = parentfamily.l_from AND" . " parentfamily.l_file = {$this->tree->getTreeId()} AND" . " birth.d_fact = 'BIRT' AND" . " childbirth.d_fact = 'BIRT' AND" . " birth.d_julianday1 <> 0 AND" . " childbirth.d_julianday2 > birth.d_julianday1" . " ORDER BY age {$age_dir} LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; if (isset($row['id'])) { $person = Individual::getInstance($row['id'], $this->tree); } switch ($type) { default: case 'full': if ($person->canShow()) { $result = $person->formatList('span', false, $person->getFullName()); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'name': $result = '' . $person->getFullName() . ''; break; case 'age': $age = $row['age']; if ($show_years) { if ((int) ($age / 365.25) > 0) { $age = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $age = (int) ($age / 30.4375) . 'm'; } else { $age = $age . 'd'; } $result = FunctionsDate::getAgeAtEvent($age); } else { $result = (int) ($age / 365.25); } break; } return $result; } /** * General query on marriages. * * @param bool $simple * @param bool $first * @param int $year1 * @param int $year2 * @param string[] $params * * @return string|array */ public function statsMarrQuery($simple = true, $first = false, $year1 = -1, $year2 = -1, $params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if ($simple) { $sql = "SELECT SQL_CACHE FLOOR(d_year/100+1) AS century, COUNT(*) AS total" . " FROM `##dates`" . " WHERE d_file={$this->tree->getTreeId()} AND d_year<>0 AND d_fact='MARR' AND d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; if ($year1 >= 0 && $year2 >= 0) { $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; } $sql .= " GROUP BY century ORDER BY century"; } elseif ($first) { $years = ''; if ($year1 >= 0 && $year2 >= 0) { $years = " married.d_year BETWEEN '{$year1}' AND '{$year2}' AND"; } $sql = " SELECT SQL_CACHE fam.f_id AS fams, fam.f_husb, fam.f_wife, married.d_julianday2 AS age, married.d_month AS month, indi.i_id AS indi" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" . " WHERE" . " married.d_gid = fam.f_id AND" . " fam.f_file = {$this->tree->getTreeId()} AND" . " married.d_fact = 'MARR' AND" . " married.d_julianday2 <> 0 AND" . $years . " (indi.i_id = fam.f_husb OR indi.i_id = fam.f_wife)" . " ORDER BY fams, indi, age ASC"; } else { $sql = "SELECT SQL_CACHE d_month, COUNT(*) AS total" . " FROM `##dates`" . " WHERE d_file={$this->tree->getTreeId()} AND d_fact='MARR'"; if ($year1 >= 0 && $year2 >= 0) { $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; } $sql .= " GROUP BY d_month"; } $rows = $this->runSql($sql); if (!isset($rows)) { return ''; } if ($simple) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } $sizes = explode('x', $size); $tot = 0; foreach ($rows as $values) { $tot += (int) $values['total']; } // Beware divide by zero if ($tot === 0) { return ''; } $centuries = ''; $counts = array(); foreach ($rows as $values) { $counts[] = round(100 * $values['total'] / $tot, 0); $centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|'; } $chd = $this->arrayToExtendedEncoding($counts); $chl = substr($centuries, 0, -1); return "\"""; } return $rows; } /** * General query on divorces. * * @param bool $simple * @param bool $first * @param int $year1 * @param int $year2 * @param string[] $params * * @return string|array */ private function statsDivQuery($simple = true, $first = false, $year1 = -1, $year2 = -1, $params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if ($simple) { $sql = "SELECT SQL_CACHE FLOOR(d_year/100+1) AS century, COUNT(*) AS total" . " FROM `##dates`" . " WHERE d_file={$this->tree->getTreeId()} AND d_year<>0 AND d_fact = 'DIV' AND d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; if ($year1 >= 0 && $year2 >= 0) { $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; } $sql .= " GROUP BY century ORDER BY century"; } elseif ($first) { $years = ''; if ($year1 >= 0 && $year2 >= 0) { $years = " divorced.d_year BETWEEN '{$year1}' AND '{$year2}' AND"; } $sql = " SELECT SQL_CACHE fam.f_id AS fams, fam.f_husb, fam.f_wife, divorced.d_julianday2 AS age, divorced.d_month AS month, indi.i_id AS indi" . " FROM `##families` AS fam" . " LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->getTreeId()}" . " WHERE" . " divorced.d_gid = fam.f_id AND" . " fam.f_file = {$this->tree->getTreeId()} AND" . " divorced.d_fact = 'DIV' AND" . " divorced.d_julianday2 <> 0 AND" . $years . " (indi.i_id = fam.f_husb OR indi.i_id = fam.f_wife)" . " ORDER BY fams, indi, age ASC"; } else { $sql = "SELECT SQL_CACHE d_month, COUNT(*) AS total FROM `##dates` " . "WHERE d_file={$this->tree->getTreeId()} AND d_fact = 'DIV'"; if ($year1 >= 0 && $year2 >= 0) { $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; } $sql .= " GROUP BY d_month"; } $rows = $this->runSql($sql); if (!isset($rows)) { return ''; } if ($simple) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } $sizes = explode('x', $size); $tot = 0; foreach ($rows as $values) { $tot += (int) $values['total']; } // Beware divide by zero if ($tot === 0) { return ''; } $centuries = ''; $counts = array(); foreach ($rows as $values) { $counts[] = round(100 * $values['total'] / $tot, 0); $centuries .= $this->centuryName($values['century']) . ' - ' . I18N::number($values['total']) . '|'; } $chd = $this->arrayToExtendedEncoding($counts); $chl = substr($centuries, 0, -1); return "\"""; } return $rows; } /** * Find the earliest marriage. * * @return string */ public function firstMarriage() { return $this->mortalityQuery('full', 'ASC', 'MARR'); } /** * Find the year of the earliest marriage. * * @return string */ public function firstMarriageYear() { return $this->mortalityQuery('year', 'ASC', 'MARR'); } /** * Find the names of spouses of the earliest marriage. * * @return string */ public function firstMarriageName() { return $this->mortalityQuery('name', 'ASC', 'MARR'); } /** * Find the place of the earliest marriage. * * @return string */ public function firstMarriagePlace() { return $this->mortalityQuery('place', 'ASC', 'MARR'); } /** * Find the latest marriage. * * @return string */ public function lastMarriage() { return $this->mortalityQuery('full', 'DESC', 'MARR'); } /** * Find the year of the latest marriage. * * @return string */ public function lastMarriageYear() { return $this->mortalityQuery('year', 'DESC', 'MARR'); } /** * Find the names of spouses of the latest marriage. * * @return string */ public function lastMarriageName() { return $this->mortalityQuery('name', 'DESC', 'MARR'); } /** * Find the location of the latest marriage. * * @return string */ public function lastMarriagePlace() { return $this->mortalityQuery('place', 'DESC', 'MARR'); } /** * General query on marriages. * * @param string[] $params * * @return string */ public function statsMarr($params = array()) { return $this->statsMarrQuery(true, false, -1, -1, $params); } /** * Find the earliest divorce. * * @return string */ public function firstDivorce() { return $this->mortalityQuery('full', 'ASC', 'DIV'); } /** * Find the year of the earliest divorce. * * @return string */ public function firstDivorceYear() { return $this->mortalityQuery('year', 'ASC', 'DIV'); } /** * Find the names of individuals in the earliest divorce. * * @return string */ public function firstDivorceName() { return $this->mortalityQuery('name', 'ASC', 'DIV'); } /** * Find the location of the earliest divorce. * * @return string */ public function firstDivorcePlace() { return $this->mortalityQuery('place', 'ASC', 'DIV'); } /** * Find the latest divorce. * * @return string */ public function lastDivorce() { return $this->mortalityQuery('full', 'DESC', 'DIV'); } /** * Find the year of the latest divorce. * * @return string */ public function lastDivorceYear() { return $this->mortalityQuery('year', 'DESC', 'DIV'); } /** * Find the names of the individuals in the latest divorce. * * @return string */ public function lastDivorceName() { return $this->mortalityQuery('name', 'DESC', 'DIV'); } /** * Find the location of the latest divorce. * * @return string */ public function lastDivorcePlace() { return $this->mortalityQuery('place', 'DESC', 'DIV'); } /** * General divorce query. * * @param string[] $params * * @return string */ public function statsDiv($params = array()) { return $this->statsDivQuery(true, false, -1, -1, $params); } /** * General query on ages at marriage. * * @param bool $simple * @param string $sex * @param int $year1 * @param int $year2 * @param string[] $params * * @return array|string */ public function statsMarrAgeQuery($simple = true, $sex = 'M', $year1 = -1, $year2 = -1, $params = array()) { if ($simple) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = '200x250'; } $sizes = explode('x', $size); $rows = $this->runSql( "SELECT SQL_CACHE " . " ROUND(AVG(married.d_julianday2-birth.d_julianday1-182.5)/365.25,1) AS age, " . " FLOOR(married.d_year/100+1) AS century, " . " 'M' AS sex " . "FROM `##dates` AS married " . "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " . "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_husb AND birth.d_file=fam.f_file) " . "WHERE " . " '{$sex}' IN ('M', 'BOTH') AND " . " married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " . " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " . " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " . "GROUP BY century, sex " . "UNION ALL " . "SELECT " . " ROUND(AVG(married.d_julianday2-birth.d_julianday1-182.5)/365.25,1) AS age, " . " FLOOR(married.d_year/100+1) AS century, " . " 'F' AS sex " . "FROM `##dates` AS married " . "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " . "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_wife AND birth.d_file=fam.f_file) " . "WHERE " . " '{$sex}' IN ('F', 'BOTH') AND " . " married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " . " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " . " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " . " GROUP BY century, sex ORDER BY century" ); if (empty($rows)) { return ''; } $max = 0; foreach ($rows as $values) { if ($max < $values['age']) { $max = $values['age']; } } $chxl = '0:|'; $chmm = ''; $chmf = ''; $i = 0; $countsm = ''; $countsf = ''; $countsa = ''; $out = array(); foreach ($rows as $values) { $out[$values['century']][$values['sex']] = $values['age']; } foreach ($out as $century => $values) { if ($sizes[0] < 1000) { $sizes[0] += 50; } $chxl .= $this->centuryName($century) . '|'; $average = 0; if (isset($values['F'])) { if ($max <= 50) { $value = $values['F'] * 2; } else { $value = $values['F']; } $countsf .= $value . ','; $average = $value; $chmf .= 't' . $values['F'] . ',000000,1,' . $i . ',11,1|'; } else { $countsf .= '0,'; $chmf .= 't0,000000,1,' . $i . ',11,1|'; } if (isset($values['M'])) { if ($max <= 50) { $value = $values['M'] * 2; } else { $value = $values['M']; } $countsm .= $value . ','; if ($average == 0) { $countsa .= $value . ','; } else { $countsa .= (($value + $average) / 2) . ','; } $chmm .= 't' . $values['M'] . ',000000,0,' . $i . ',11,1|'; } else { $countsm .= '0,'; if ($average == 0) { $countsa .= '0,'; } else { $countsa .= $value . ','; } $chmm .= 't0,000000,0,' . $i . ',11,1|'; } $i++; } $countsm = substr($countsm, 0, -1); $countsf = substr($countsf, 0, -1); $countsa = substr($countsa, 0, -1); $chmf = substr($chmf, 0, -1); $chd = 't2:' . $countsm . '|' . $countsf . '|' . $countsa; if ($max <= 50) { $chxl .= '1:||' . I18N::translate('century') . '|2:|0|10|20|30|40|50|3:||' . I18N::translate('Age') . '|'; } else { $chxl .= '1:||' . I18N::translate('century') . '|2:|0|10|20|30|40|50|60|70|80|90|100|3:||' . I18N::translate('Age') . '|'; } if (count($rows) > 4 || mb_strlen(I18N::translate('Average age in century of marriage')) < 30) { $chtt = I18N::translate('Average age in century of marriage'); } else { $offset = 0; $counter = array(); while ($offset = strpos(I18N::translate('Average age in century of marriage'), ' ', $offset + 1)) { $counter[] = $offset; } $half = (int) (count($counter) / 2); $chtt = substr_replace(I18N::translate('Average age in century of marriage'), '|', $counter[$half], 1); } return "\"""; } else { if ($year1 >= 0 && $year2 >= 0) { $years = " married.d_year BETWEEN {$year1} AND {$year2} AND "; } else { $years = ''; } $rows = $this->runSql( "SELECT SQL_CACHE " . " fam.f_id, " . " birth.d_gid, " . " married.d_julianday2-birth.d_julianday1 AS age " . "FROM `##dates` AS married " . "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " . "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_husb AND birth.d_file=fam.f_file) " . "WHERE " . " '{$sex}' IN ('M', 'BOTH') AND {$years} " . " married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " . " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " . " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " . "UNION ALL " . "SELECT " . " fam.f_id, " . " birth.d_gid, " . " married.d_julianday2-birth.d_julianday1 AS age " . "FROM `##dates` AS married " . "JOIN `##families` AS fam ON (married.d_gid=fam.f_id AND married.d_file=fam.f_file) " . "JOIN `##dates` AS birth ON (birth.d_gid=fam.f_wife AND birth.d_file=fam.f_file) " . "WHERE " . " '{$sex}' IN ('F', 'BOTH') AND {$years} " . " married.d_file={$this->tree->getTreeId()} AND married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND married.d_fact='MARR' AND " . " birth.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND birth.d_fact='BIRT' AND " . " married.d_julianday1>birth.d_julianday1 AND birth.d_julianday1<>0 " ); return $rows; } } /** * Find the youngest wife. * * @return string */ public function youngestMarriageFemale() { return $this->marriageQuery('full', 'ASC', 'F', false); } /** * Find the name of the youngest wife. * * @return string */ public function youngestMarriageFemaleName() { return $this->marriageQuery('name', 'ASC', 'F', false); } /** * Find the age of the youngest wife. * * @param bool $show_years * * @return string */ public function youngestMarriageFemaleAge($show_years = false) { return $this->marriageQuery('age', 'ASC', 'F', $show_years); } /** * Find the oldest wife. * * @return string */ public function oldestMarriageFemale() { return $this->marriageQuery('full', 'DESC', 'F', false); } /** * Find the name of the oldest wife. * * @return string */ public function oldestMarriageFemaleName() { return $this->marriageQuery('name', 'DESC', 'F', false); } /** * Find the age of the oldest wife. * * @param bool $show_years * * @return string */ public function oldestMarriageFemaleAge($show_years = false) { return $this->marriageQuery('age', 'DESC', 'F', $show_years); } /** * Find the youngest husband. * * @return string */ public function youngestMarriageMale() { return $this->marriageQuery('full', 'ASC', 'M', false); } /** * Find the name of the youngest husband. * * @return string */ public function youngestMarriageMaleName() { return $this->marriageQuery('name', 'ASC', 'M', false); } /** * Find the age of the youngest husband. * * @param bool $show_years * * @return string */ public function youngestMarriageMaleAge($show_years = false) { return $this->marriageQuery('age', 'ASC', 'M', $show_years); } /** * Find the oldest husband. * * @return string */ public function oldestMarriageMale() { return $this->marriageQuery('full', 'DESC', 'M', false); } /** * Find the name of the oldest husband. * * @return string */ public function oldestMarriageMaleName() { return $this->marriageQuery('name', 'DESC', 'M', false); } /** * Find the age of the oldest husband. * * @param bool $show_years * * @return string */ public function oldestMarriageMaleAge($show_years = false) { return $this->marriageQuery('age', 'DESC', 'M', $show_years); } /** * General query on marriage ages. * * @param string[] $params * * @return string */ public function statsMarrAge($params = array()) { return $this->statsMarrAgeQuery(true, 'BOTH', -1, -1, $params); } /** * Find the age between husband and wife. * * @param string[] $params * * @return string */ public function ageBetweenSpousesMF($params = array()) { return $this->ageBetweenSpousesQuery('nolist', 'DESC', $params); } /** * Find the age between husband and wife. * * @param string[] $params * * @return string */ public function ageBetweenSpousesMFList($params = array()) { return $this->ageBetweenSpousesQuery('list', 'DESC', $params); } /** * Find the age between wife and husband.. * * @param string[] $params * * @return string */ public function ageBetweenSpousesFM($params = array()) { return $this->ageBetweenSpousesQuery('nolist', 'ASC', $params); } /** * Find the age between wife and husband.. * * @param string[] $params * * @return string */ public function ageBetweenSpousesFMList($params = array()) { return $this->ageBetweenSpousesQuery('list', 'ASC', $params); } /** * General query on marriage ages. * * @return string */ public function topAgeOfMarriageFamily() { return $this->ageOfMarriageQuery('name', 'DESC', array('1')); } /** * General query on marriage ages. * * @return string */ public function topAgeOfMarriage() { return $this->ageOfMarriageQuery('age', 'DESC', array('1')); } /** * General query on marriage ages. * * @param string[] $params * * @return string */ public function topAgeOfMarriageFamilies($params = array()) { return $this->ageOfMarriageQuery('nolist', 'DESC', $params); } /** * General query on marriage ages. * * @param string[] $params * * @return string */ public function topAgeOfMarriageFamiliesList($params = array()) { return $this->ageOfMarriageQuery('list', 'DESC', $params); } /** * General query on marriage ages. * * @return string */ public function minAgeOfMarriageFamily() { return $this->ageOfMarriageQuery('name', 'ASC', array('1')); } /** * General query on marriage ages. * * @return string */ public function minAgeOfMarriage() { return $this->ageOfMarriageQuery('age', 'ASC', array('1')); } /** * General query on marriage ages. * * @param string[] $params * * @return string */ public function minAgeOfMarriageFamilies($params = array()) { return $this->ageOfMarriageQuery('nolist', 'ASC', $params); } /** * General query on marriage ages. * * @param string[] $params * * @return string */ public function minAgeOfMarriageFamiliesList($params = array()) { return $this->ageOfMarriageQuery('list', 'ASC', $params); } /** * Find the youngest mother * * @return string */ public function youngestMother() { return $this->parentsQuery('full', 'ASC', 'F'); } /** * Find the name of the youngest mother. * * @return string */ public function youngestMotherName() { return $this->parentsQuery('name', 'ASC', 'F'); } /** * Find the age of the youngest mother. * * @param bool $show_years * * @return string */ public function youngestMotherAge($show_years = false) { return $this->parentsQuery('age', 'ASC', 'F', $show_years); } /** * Find the oldest mother. * * @return string */ public function oldestMother() { return $this->parentsQuery('full', 'DESC', 'F'); } /** * Find the name of the oldest mother. * * @return string */ public function oldestMotherName() { return $this->parentsQuery('name', 'DESC', 'F'); } /** * Find the age of the oldest mother. * * @param bool $show_years * * @return string */ public function oldestMotherAge($show_years = false) { return $this->parentsQuery('age', 'DESC', 'F', $show_years); } /** * Find the youngest father. * * @return string */ public function youngestFather() { return $this->parentsQuery('full', 'ASC', 'M'); } /** * Find the name of the youngest father. * * @return string */ public function youngestFatherName() { return $this->parentsQuery('name', 'ASC', 'M'); } /** * Find the age of the youngest father. * * @param bool $show_years * * @return string */ public function youngestFatherAge($show_years = false) { return $this->parentsQuery('age', 'ASC', 'M', $show_years); } /** * Find the oldest father. * * @return string */ public function oldestFather() { return $this->parentsQuery('full', 'DESC', 'M'); } /** * Find the name of the oldest father. * * @return string */ public function oldestFatherName() { return $this->parentsQuery('name', 'DESC', 'M'); } /** * Find the age of the oldest father. * * @param bool $show_years * * @return string */ public function oldestFatherAge($show_years = false) { return $this->parentsQuery('age', 'DESC', 'M', $show_years); } /** * Number of husbands. * * @return string */ public function totalMarriedMales() { $n = Database::prepare("SELECT SQL_CACHE COUNT(DISTINCT f_husb) FROM `##families` WHERE f_file=? AND f_gedcom LIKE '%\\n1 MARR%'") ->execute(array($this->tree->getTreeId())) ->fetchOne(); return I18N::number($n); } /** * Number of wives. * * @return string */ public function totalMarriedFemales() { $n = Database::prepare("SELECT SQL_CACHE COUNT(DISTINCT f_wife) FROM `##families` WHERE f_file=? AND f_gedcom LIKE '%\\n1 MARR%'") ->execute(array($this->tree->getTreeId())) ->fetchOne(); return I18N::number($n); } /** * General query on family. * * @param string $type * * @return string */ private function familyQuery($type = 'full') { $rows = $this->runSql( " SELECT SQL_CACHE f_numchil AS tot, f_id AS id" . " FROM `##families`" . " WHERE" . " f_file={$this->tree->getTreeId()}" . " AND f_numchil = (" . " SELECT max( f_numchil )" . " FROM `##families`" . " WHERE f_file ={$this->tree->getTreeId()}" . " )" . " LIMIT 1" ); if (!isset($rows[0])) { return ''; } $row = $rows[0]; $family = Family::getInstance($row['id'], $this->tree); switch ($type) { default: case 'full': if ($family->canShow()) { $result = $family->formatList('span', false, $family->getFullName()); } else { $result = I18N::translate('This information is private and cannot be shown.'); } break; case 'size': $result = I18N::number($row['tot']); break; case 'name': $result = "getHtmlUrl() . "\">" . $family->getFullName() . ''; break; } return $result; } /** * General query on families. * * @param string $type * @param string[] $params * * @return string */ private function topTenFamilyQuery($type = 'list', $params = array()) { if (isset($params[0])) { $total = (int) $params[0]; } else { $total = 10; } $rows = $this->runSql( "SELECT SQL_CACHE f_numchil AS tot, f_id AS id" . " FROM `##families`" . " WHERE" . " f_file={$this->tree->getTreeId()}" . " ORDER BY tot DESC" . " LIMIT " . $total ); if (!isset($rows[0])) { return ''; } if (count($rows) < $total) { $total = count($rows); } $top10 = array(); for ($c = 0; $c < $total; $c++) { $family = Family::getInstance($rows[$c]['id'], $this->tree); if ($family->canShow()) { if ($type === 'list') { $top10[] = '
  • ' . $family->getFullName() . ' - ' . I18N::plural('%s child', '%s children', $rows[$c]['tot'], I18N::number($rows[$c]['tot'])); } else { $top10[] = '' . $family->getFullName() . ' - ' . I18N::plural('%s child', '%s children', $rows[$c]['tot'], I18N::number($rows[$c]['tot'])); } } } if ($type === 'list') { $top10 = implode('', $top10); } else { $top10 = implode('; ', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace(array('[', ']', '(', ')', '+'), array('‏[', '‏]', '‏(', '‏)', '‏+'), $top10); } if ($type === 'list') { return ''; } return $top10; } /** * Find the ages between siblings. * * @param string $type * @param string[] $params * * @return string */ private function ageBetweenSiblingsQuery($type = 'list', $params = array()) { if (isset($params[0])) { $total = (int) $params[0]; } else { $total = 10; } if (isset($params[1])) { $one = $params[1]; } else { $one = false; } // each family only once if true $rows = $this->runSql( " SELECT SQL_CACHE DISTINCT" . " link1.l_from AS family," . " link1.l_to AS ch1," . " link2.l_to AS ch2," . " child1.d_julianday2-child2.d_julianday2 AS age" . " FROM `##link` AS link1" . " LEFT JOIN `##dates` AS child1 ON child1.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##dates` AS child2 ON child2.d_file = {$this->tree->getTreeId()}" . " LEFT JOIN `##link` AS link2 ON link2.l_file = {$this->tree->getTreeId()}" . " WHERE" . " link1.l_file = {$this->tree->getTreeId()} AND" . " link1.l_from = link2.l_from AND" . " link1.l_type = 'CHIL' AND" . " child1.d_gid = link1.l_to AND" . " child1.d_fact = 'BIRT' AND" . " link2.l_type = 'CHIL' AND" . " child2.d_gid = link2.l_to AND" . " child2.d_fact = 'BIRT' AND" . " child1.d_julianday2 > child2.d_julianday2 AND" . " child2.d_julianday2 <> 0 AND" . " child1.d_gid <> child2.d_gid" . " ORDER BY age DESC" . " LIMIT " . $total ); if (!isset($rows[0])) { return ''; } $top10 = array(); $dist = array(); foreach ($rows as $fam) { $family = Family::getInstance($fam['family'], $this->tree); $child1 = Individual::getInstance($fam['ch1'], $this->tree); $child2 = Individual::getInstance($fam['ch2'], $this->tree); if ($type == 'name') { if ($child1->canShow() && $child2->canShow()) { $return = '' . $child2->getFullName() . ' '; $return .= I18N::translate('and') . ' '; $return .= '' . $child1->getFullName() . ''; $return .= ' [' . I18N::translate('View this family') . ']'; } else { $return = I18N::translate('This information is private and cannot be shown.'); } return $return; } $age = $fam['age']; if ((int) ($age / 365.25) > 0) { $age = (int) ($age / 365.25) . 'y'; } elseif ((int) ($age / 30.4375) > 0) { $age = (int) ($age / 30.4375) . 'm'; } else { $age = $age . 'd'; } $age = FunctionsDate::getAgeAtEvent($age); if ($type == 'age') { return $age; } if ($type == 'list') { if ($one && !in_array($fam['family'], $dist)) { if ($child1->canShow() && $child2->canShow()) { $return = "
  • "; $return .= "getHtmlUrl() . "\">" . $child2->getFullName() . " "; $return .= I18N::translate('and') . " "; $return .= "getHtmlUrl() . "\">" . $child1->getFullName() . ""; $return .= " (" . $age . ")"; $return .= " getHtmlUrl() . "\">[" . I18N::translate('View this family') . "]"; $return .= '
  • '; $top10[] = $return; $dist[] = $fam['family']; } } elseif (!$one && $child1->canShow() && $child2->canShow()) { $return = "
  • "; $return .= "getHtmlUrl() . "\">" . $child2->getFullName() . " "; $return .= I18N::translate('and') . " "; $return .= "getHtmlUrl() . "\">" . $child1->getFullName() . ""; $return .= " (" . $age . ")"; $return .= " getHtmlUrl() . "\">[" . I18N::translate('View this family') . "]"; $return .= '
  • '; $top10[] = $return; } } else { if ($child1->canShow() && $child2->canShow()) { $return = $child2->formatList('span', false, $child2->getFullName()); $return .= "
    " . I18N::translate('and') . "
    "; $return .= $child1->formatList('span', false, $child1->getFullName()); $return .= "
    getHtmlUrl() . "\">[" . I18N::translate('View this family') . "]"; return $return; } else { return I18N::translate('This information is private and cannot be shown.'); } } } if ($type === 'list') { $top10 = implode('', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace(array('[', ']', '(', ')', '+'), array('‏[', '‏]', '‏(', '‏)', '‏+'), $top10); } if ($type === 'list') { return ''; } return $top10; } /** * Find the month in the year of the birth of the first child. * * @param bool $simple * @param bool $sex * @param int $year1 * @param int $year2 * @param string[] $params * * @return string|string[][] */ public function monthFirstChildQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, $params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if ($year1 >= 0 && $year2 >= 0) { $sql_years = " AND (d_year BETWEEN '{$year1}' AND '{$year2}')"; } else { $sql_years = ''; } if ($sex) { $sql_sex1 = ', i_sex'; $sql_sex2 = " JOIN `##individuals` AS child ON child1.d_file = i_file AND child1.d_gid = child.i_id "; } else { $sql_sex1 = ''; $sql_sex2 = ''; } $sql = "SELECT SQL_CACHE d_month{$sql_sex1}, COUNT(*) AS total " . "FROM (" . " SELECT family{$sql_sex1}, MIN(date) AS d_date, d_month" . " FROM (" . " SELECT" . " link1.l_from AS family," . " link1.l_to AS child," . " child1.d_julianday2 as date," . " child1.d_month as d_month" . $sql_sex1 . " FROM `##link` AS link1" . " LEFT JOIN `##dates` AS child1 ON child1.d_file = {$this->tree->getTreeId()}" . $sql_sex2 . " WHERE" . " link1.l_file = {$this->tree->getTreeId()} AND" . " link1.l_type = 'CHIL' AND" . " child1.d_gid = link1.l_to AND" . " child1.d_fact = 'BIRT' AND" . " d_type IN ('@#DGREGORIAN@', '@#DJULIAN@') AND" . " child1.d_month <> ''" . $sql_years . " ORDER BY date" . " ) AS children" . " GROUP BY family" . ") AS first_child " . "GROUP BY d_month"; if ($sex) { $sql .= ', i_sex'; } $rows = $this->runSql($sql); if ($simple) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } $sizes = explode('x', $size); $tot = 0; foreach ($rows as $values) { $tot += $values['total']; } // Beware divide by zero if ($tot == 0) { return ''; } $text = ''; $counts = array(); foreach ($rows as $values) { $counts[] = round(100 * $values['total'] / $tot, 0); switch ($values['d_month']) { default: case 'JAN': $values['d_month'] = 1; break; case 'FEB': $values['d_month'] = 2; break; case 'MAR': $values['d_month'] = 3; break; case 'APR': $values['d_month'] = 4; break; case 'MAY': $values['d_month'] = 5; break; case 'JUN': $values['d_month'] = 6; break; case 'JUL': $values['d_month'] = 7; break; case 'AUG': $values['d_month'] = 8; break; case 'SEP': $values['d_month'] = 9; break; case 'OCT': $values['d_month'] = 10; break; case 'NOV': $values['d_month'] = 11; break; case 'DEC': $values['d_month'] = 12; break; } $text .= I18N::translate(ucfirst(strtolower(($values['d_month'])))) . ' - ' . $values['total'] . '|'; } $chd = $this->arrayToExtendedEncoding($counts); $chl = substr($text, 0, -1); return '' . I18N::translate('Month of birth of first child in a relation') . ''; } return $rows; } /** * Find the family with the most children. * * @return string */ public function largestFamily() { return $this->familyQuery('full'); } /** * Find the number of children in the largest family. * * @return string */ public function largestFamilySize() { return $this->familyQuery('size'); } /** * Find the family with the most children. * * @return string */ public function largestFamilyName() { return $this->familyQuery('name'); } /** * The the families with the most children. * * @param string[] $params * * @return string */ public function topTenLargestFamily($params = array()) { return $this->topTenFamilyQuery('nolist', $params); } /** * Find the families with the most children. * * @param string[] $params * * @return string */ public function topTenLargestFamilyList($params = array()) { return $this->topTenFamilyQuery('list', $params); } /** * Create a chart of the largest families. * * @param string[] $params * * @return string */ public function chartLargestFamilies($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_L_CHART_X = Theme::theme()->parameter('stats-large-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_L_CHART_X . 'x' . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } if (isset($params[3]) && $params[3] != '') { $total = strtolower($params[3]); } else { $total = 10; } $sizes = explode('x', $size); $total = (int) $total; $rows = $this->runSql( " SELECT SQL_CACHE f_numchil AS tot, f_id AS id" . " FROM `##families`" . " WHERE f_file={$this->tree->getTreeId()}" . " ORDER BY tot DESC" . " LIMIT " . $total ); if (!isset($rows[0])) { return ''; } $tot = 0; foreach ($rows as $row) { $tot += (int) $row['tot']; } $chd = ''; $chl = array(); foreach ($rows as $row) { $family = Family::getInstance($row['id'], $this->tree); if ($family->canShow()) { if ($tot == 0) { $per = 0; } else { $per = round(100 * $row['tot'] / $tot, 0); } $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = htmlspecialchars_decode(strip_tags($family->getFullName())) . ' - ' . I18N::number($row['tot']); } } $chl = rawurlencode(implode('|', $chl)); return "\"""; } /** * Count the total children. * * @return string */ public function totalChildren() { $rows = $this->runSql("SELECT SQL_CACHE SUM(f_numchil) AS tot FROM `##families` WHERE f_file={$this->tree->getTreeId()}"); return I18N::number($rows[0]['tot']); } /** * Find the average number of children in families. * * @return string */ public function averageChildren() { $rows = $this->runSql("SELECT SQL_CACHE AVG(f_numchil) AS tot FROM `##families` WHERE f_file={$this->tree->getTreeId()}"); return I18N::number($rows[0]['tot'], 2); } /** * General query on familes/children. * * @param bool $simple * @param string $sex * @param int $year1 * @param int $year2 * @param string[] $params * * @return string|string[][] */ public function statsChildrenQuery($simple = true, $sex = 'BOTH', $year1 = -1, $year2 = -1, $params = array()) { if ($simple) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = '220x200'; } $sizes = explode('x', $size); $max = 0; $rows = $this->runSql( " SELECT SQL_CACHE ROUND(AVG(f_numchil),2) AS num, FLOOR(d_year/100+1) AS century" . " FROM `##families`" . " JOIN `##dates` ON (d_file = f_file AND d_gid=f_id)" . " WHERE f_file = {$this->tree->getTreeId()}" . " AND d_julianday1<>0" . " AND d_fact = 'MARR'" . " AND d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')" . " GROUP BY century" . " ORDER BY century"); if (empty($rows)) { return ''; } foreach ($rows as $values) { if ($max < $values['num']) { $max = $values['num']; } } $chm = ""; $chxl = "0:|"; $i = 0; $counts = array(); foreach ($rows as $values) { if ($sizes[0] < 980) { $sizes[0] += 38; } $chxl .= $this->centuryName($values['century']) . "|"; if ($max <= 5) { $counts[] = round($values['num'] * 819.2 - 1, 1); } elseif ($max <= 10) { $counts[] = round($values['num'] * 409.6, 1); } else { $counts[] = round($values['num'] * 204.8, 1); } $chm .= 't' . $values['num'] . ',000000,0,' . $i . ',11,1|'; $i++; } $chd = $this->arrayToExtendedEncoding($counts); $chm = substr($chm, 0, -1); if ($max <= 5) { $chxl .= "1:||" . I18N::translate('century') . "|2:|0|1|2|3|4|5|3:||" . I18N::translate('Number of children') . "|"; } elseif ($max <= 10) { $chxl .= "1:||" . I18N::translate('century') . "|2:|0|1|2|3|4|5|6|7|8|9|10|3:||" . I18N::translate('Number of children') . "|"; } else { $chxl .= "1:||" . I18N::translate('century') . "|2:|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|3:||" . I18N::translate('Number of children') . "|"; } return "\"""; } else { if ($sex == 'M') { $sql = "SELECT SQL_CACHE num, COUNT(*) AS total FROM " . "(SELECT count(i_sex) AS num FROM `##link` " . "LEFT OUTER JOIN `##individuals` " . "ON l_from=i_id AND l_file=i_file AND i_sex='M' AND l_type='FAMC' " . "JOIN `##families` ON f_file=l_file AND f_id=l_to WHERE f_file={$this->tree->getTreeId()} GROUP BY l_to" . ") boys" . " GROUP BY num" . " ORDER BY num"; } elseif ($sex == 'F') { $sql = "SELECT SQL_CACHE num, COUNT(*) AS total FROM " . "(SELECT count(i_sex) AS num FROM `##link` " . "LEFT OUTER JOIN `##individuals` " . "ON l_from=i_id AND l_file=i_file AND i_sex='F' AND l_type='FAMC' " . "JOIN `##families` ON f_file=l_file AND f_id=l_to WHERE f_file={$this->tree->getTreeId()} GROUP BY l_to" . ") girls" . " GROUP BY num" . " ORDER BY num"; } else { $sql = "SELECT SQL_CACHE f_numchil, COUNT(*) AS total FROM `##families` "; if ($year1 >= 0 && $year2 >= 0) { $sql .= "AS fam LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->getTreeId()}" . " WHERE" . " married.d_gid = fam.f_id AND" . " fam.f_file = {$this->tree->getTreeId()} AND" . " married.d_fact = 'MARR' AND" . " married.d_year BETWEEN '{$year1}' AND '{$year2}'"; } else { $sql .= "WHERE f_file={$this->tree->getTreeId()}"; } $sql .= " GROUP BY f_numchil"; } $rows = $this->runSql($sql); return $rows; } } /** * Genearl query on families/children. * * @param string[] $params * * @return string */ public function statsChildren($params = array()) { return $this->statsChildrenQuery(true, 'BOTH', -1, -1, $params); } /** * Find the names of siblings with the widest age gap. * * @param string[] $params * * @return string */ public function topAgeBetweenSiblingsName($params = array()) { return $this->ageBetweenSiblingsQuery('name', $params); } /** * Find the widest age gap between siblings. * * @param string[] $params * * @return string */ public function topAgeBetweenSiblings($params = array()) { return $this->ageBetweenSiblingsQuery('age', $params); } /** * Find the name of siblings with the widest age gap. * * @param string[] $params * * @return string */ public function topAgeBetweenSiblingsFullName($params = array()) { return $this->ageBetweenSiblingsQuery('nolist', $params); } /** * Find the siblings with the widest age gaps. * * @param string[] $params * * @return string */ public function topAgeBetweenSiblingsList($params = array()) { return $this->ageBetweenSiblingsQuery('list', $params); } /** * Find the families with no children. * * @return string */ private function noChildrenFamiliesQuery() { $rows = $this->runSql( " SELECT SQL_CACHE COUNT(*) AS tot" . " FROM `##families`" . " WHERE f_numchil = 0 AND f_file = {$this->tree->getTreeId()}"); return $rows[0]['tot']; } /** * Find the families with no children. * * @return string */ public function noChildrenFamilies() { return I18N::number($this->noChildrenFamiliesQuery()); } /** * Find the families with no children. * * @param string[] $params * * @return string */ public function noChildrenFamiliesList($params = array()) { if (isset($params[0]) && $params[0] != '') { $type = strtolower($params[0]); } else { $type = 'list'; } $rows = $this->runSql( " SELECT SQL_CACHE f_id AS family" . " FROM `##families` AS fam" . " WHERE f_numchil = 0 AND fam.f_file = {$this->tree->getTreeId()}"); if (!isset($rows[0])) { return ''; } $top10 = array(); foreach ($rows as $row) { $family = Family::getInstance($row['family'], $this->tree); if ($family->canShow()) { if ($type == 'list') { $top10[] = "
  • getHtmlUrl() . "\">" . $family->getFullName() . "
  • "; } else { $top10[] = "getHtmlUrl() . "\">" . $family->getFullName() . ""; } } } if ($type == 'list') { $top10 = implode('', $top10); } else { $top10 = implode('; ', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace(array('[', ']', '(', ')', '+'), array('‏[', '‏]', '‏(', '‏)', '‏+'), $top10); } if ($type === 'list') { return ''; } return $top10; } /** * Create a chart of children with no families. * * @param string[] $params * * @return string */ public function chartNoChildrenFamilies($params = array()) { if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = '220x200'; } if (isset($params[1]) && $params[1] != '') { $year1 = $params[1]; } else { $year1 = -1; } if (isset($params[2]) && $params[2] != '') { $year2 = $params[2]; } else { $year2 = -1; } $sizes = explode('x', $size); if ($year1 >= 0 && $year2 >= 0) { $years = " married.d_year BETWEEN '{$year1}' AND '{$year2}' AND"; } else { $years = ""; } $max = 0; $tot = 0; $rows = $this->runSql( "SELECT SQL_CACHE" . " COUNT(*) AS count," . " FLOOR(married.d_year/100+1) AS century" . " FROM" . " `##families` AS fam" . " JOIN" . " `##dates` AS married ON (married.d_file = fam.f_file AND married.d_gid = fam.f_id)" . " WHERE" . " f_numchil = 0 AND" . " fam.f_file = {$this->tree->getTreeId()} AND" . $years . " married.d_fact = 'MARR' AND" . " married.d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')" . " GROUP BY century ORDER BY century" ); if (empty($rows)) { return ''; } foreach ($rows as $values) { if ($max < $values['count']) { $max = $values['count']; } $tot += (int) $values['count']; } $unknown = $this->noChildrenFamiliesQuery() - $tot; if ($unknown > $max) { $max = $unknown; } $chm = ""; $chxl = "0:|"; $i = 0; $counts = array(); foreach ($rows as $values) { if ($sizes[0] < 980) { $sizes[0] += 38; } $chxl .= $this->centuryName($values['century']) . "|"; $counts[] = round(4095 * $values['count'] / ($max + 1)); $chm .= 't' . $values['count'] . ',000000,0,' . $i . ',11,1|'; $i++; } $counts[] = round(4095 * $unknown / ($max + 1)); $chd = $this->arrayToExtendedEncoding($counts); $chm .= 't' . $unknown . ',000000,0,' . $i . ',11,1'; $chxl .= I18N::translateContext('unknown century', 'Unknown') . "|1:||" . I18N::translate('century') . "|2:|0|"; $step = $max + 1; for ($d = (int) ($max + 1); $d > 0; $d--) { if (($max + 1) < ($d * 10 + 1) && fmod(($max + 1), $d) == 0) { $step = $d; } } if ($step == (int) ($max + 1)) { for ($d = (int) ($max); $d > 0; $d--) { if ($max < ($d * 10 + 1) && fmod($max, $d) == 0) { $step = $d; } } } for ($n = $step; $n <= ($max + 1); $n += $step) { $chxl .= $n . "|"; } $chxl .= "3:||" . I18N::translate('Total families') . "|"; return "\"""; } /** * Find the couple with the most grandchildren. * * @param string $type * @param string[] $params * * @return string */ private function topTenGrandFamilyQuery($type = 'list', $params = array()) { if (isset($params[0])) { $total = (int) $params[0]; } else { $total = 10; } $rows = $this->runSql( "SELECT SQL_CACHE COUNT(*) AS tot, f_id AS id" . " FROM `##families`" . " JOIN `##link` AS children ON children.l_file = {$this->tree->getTreeId()}" . " JOIN `##link` AS mchildren ON mchildren.l_file = {$this->tree->getTreeId()}" . " JOIN `##link` AS gchildren ON gchildren.l_file = {$this->tree->getTreeId()}" . " WHERE" . " f_file={$this->tree->getTreeId()} AND" . " children.l_from=f_id AND" . " children.l_type='CHIL' AND" . " children.l_to=mchildren.l_from AND" . " mchildren.l_type='FAMS' AND" . " mchildren.l_to=gchildren.l_from AND" . " gchildren.l_type='CHIL'" . " GROUP BY id" . " ORDER BY tot DESC" . " LIMIT " . $total ); if (!isset($rows[0])) { return ''; } $top10 = array(); foreach ($rows as $row) { $family = Family::getInstance($row['id'], $this->tree); if ($family->canShow()) { if ($type === 'list') { $top10[] = '
  • ' . $family->getFullName() . ' - ' . I18N::plural('%s grandchild', '%s grandchildren', $row['tot'], I18N::number($row['tot'])); } else { $top10[] = '' . $family->getFullName() . ' - ' . I18N::plural('%s grandchild', '%s grandchildren', $row['tot'], I18N::number($row['tot'])); } } } if ($type === 'list') { $top10 = implode('', $top10); } else { $top10 = implode('; ', $top10); } if (I18N::direction() === 'rtl') { $top10 = str_replace(array('[', ']', '(', ')', '+'), array('‏[', '‏]', '‏(', '‏)', '‏+'), $top10); } if ($type === 'list') { return ''; } return $top10; } /** * Find the couple with the most grandchildren. * * @param string[] $params * * @return string */ public function topTenLargestGrandFamily($params = array()) { return $this->topTenGrandFamilyQuery('nolist', $params); } /** * Find the couple with the most grandchildren. * * @param string[] $params * * @return string */ public function topTenLargestGrandFamilyList($params = array()) { return $this->topTenGrandFamilyQuery('list', $params); } /** * Find common surnames. * * @param string $type * @param bool $show_tot * @param string[] $params * * @return string */ private function commonSurnamesQuery($type = 'list', $show_tot = false, $params = array()) { $threshold = empty($params[0]) ? 10 : (int) $params[0]; $number_of_surnames = empty($params[1]) ? 10 : (int) $params[1]; $sorting = empty($params[2]) ? 'alpha' : $params[2]; $surname_list = FunctionsDb::getTopSurnames($this->tree->getTreeId(), $threshold, $number_of_surnames); if (empty($surname_list)) { return ''; } switch ($sorting) { default: case 'alpha': uksort($surname_list, '\Fisharebest\Webtrees\I18N::strcasecmp'); break; case 'count': asort($surname_list); break; case 'rcount': arsort($surname_list); break; } // Note that we count/display SPFX SURN, but sort/group under just SURN $surnames = array(); foreach (array_keys($surname_list) as $surname) { $surnames = array_merge($surnames, QueryName::surnames($this->tree, $surname, '', false, false)); } return FunctionsPrintLists::surnameList($surnames, ($type == 'list' ? 1 : 2), $show_tot, 'indilist.php', $this->tree); } /** * Find common surnames. * * @return string */ public function getCommonSurname() { $surnames = array_keys(FunctionsDb::getTopSurnames($this->tree->getTreeId(), 1, 1)); return array_shift($surnames); } /** * Find common surnames. * * @param string[] $params * * @return string */ public function commonSurnames($params = array('', '', 'alpha')) { return $this->commonSurnamesQuery('nolist', false, $params); } /** * Find common surnames. * * @param string[] $params * * @return string */ public function commonSurnamesTotals($params = array('', '', 'rcount')) { return $this->commonSurnamesQuery('nolist', true, $params); } /** * Find common surnames. * * @param string[] $params * * @return string */ public function commonSurnamesList($params = array('', '', 'alpha')) { return $this->commonSurnamesQuery('list', false, $params); } /** * Find common surnames. * * @param string[] $params * * @return string */ public function commonSurnamesListTotals($params = array('', '', 'rcount')) { return $this->commonSurnamesQuery('list', true, $params); } /** * Create a chart of common surnames. * * @param string[] $params * * @return string */ public function chartCommonSurnames($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); $size = empty($params[0]) ? $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y : strtolower($params[0]); $color_from = empty($params[1]) ? $WT_STATS_CHART_COLOR1 : strtolower($params[1]); $color_to = empty($params[2]) ? $WT_STATS_CHART_COLOR2 : strtolower($params[2]); $number_of_surnames = empty($params[3]) ? 10 : (int) $params[3]; $sizes = explode('x', $size); $tot_indi = $this->totalIndividualsQuery(); $surnames = FunctionsDb::getTopSurnames($this->tree->getTreeId(), 0, $number_of_surnames); if (empty($surnames)) { return ''; } $SURNAME_TRADITION = $this->tree->getPreference('SURNAME_TRADITION'); $all_surnames = array(); $tot = 0; foreach ($surnames as $surname => $num) { $all_surnames = array_merge($all_surnames, QueryName::surnames($this->tree, I18N::strtoupper($surname), '', false, false)); $tot += $num; } $chd = ''; $chl = array(); foreach ($all_surnames as $surns) { $count_per = 0; $max_name = 0; $top_name = ''; foreach ($surns as $spfxsurn => $indis) { $per = count($indis); $count_per += $per; // select most common surname from all variants if ($per > $max_name) { $max_name = $per; $top_name = $spfxsurn; } } switch ($SURNAME_TRADITION) { case 'polish': // most common surname should be in male variant (Kowalski, not Kowalska) $top_name = preg_replace(array('/ska$/', '/cka$/', '/dzka$/', '/żka$/'), array('ski', 'cki', 'dzki', 'żki'), $top_name); } $per = round(100 * $count_per / $tot_indi, 0); $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = $top_name . ' - ' . I18N::number($count_per); } $per = round(100 * ($tot_indi - $tot) / $tot_indi, 0); $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot); $chart_title = implode(I18N::$list_separator, $chl); $chl = implode('|', $chl); return '' . $chart_title . ''; } /** * Find common given names. * * @param string $sex * @param string $type * @param bool $show_tot * @param string[] $params * * @return string */ private function commonGivenQuery($sex = 'B', $type = 'list', $show_tot = false, $params = array()) { if (isset($params[0]) && $params[0] != '' && $params[0] >= 0) { $threshold = (int) $params[0]; } else { $threshold = 1; } if (isset($params[1]) && $params[1] != '' && $params[1] >= 0) { $maxtoshow = (int) $params[1]; } else { $maxtoshow = 10; } switch ($sex) { case 'M': $sex_sql = "i_sex='M'"; break; case 'F': $sex_sql = "i_sex='F'"; break; case 'U': $sex_sql = "i_sex='U'"; break; case 'B': default: $sex_sql = "i_sex<>'U'"; break; } $ged_id = $this->tree->getTreeId(); $rows = Database::prepare("SELECT SQL_CACHE n_givn, COUNT(*) AS num FROM `##name` JOIN `##individuals` ON (n_id=i_id AND n_file=i_file) WHERE n_file={$ged_id} AND n_type<>'_MARNM' AND n_givn NOT IN ('@P.N.', '') AND LENGTH(n_givn)>1 AND {$sex_sql} GROUP BY n_id, n_givn") ->fetchAll(); $nameList = array(); foreach ($rows as $row) { // Split “John Thomas” into “John” and “Thomas” and count against both totals foreach (explode(' ', $row->n_givn) as $given) { // Exclude initials and particles. if (!preg_match('/^([A-Z]|[a-z]{1,3})$/', $given)) { if (array_key_exists($given, $nameList)) { $nameList[$given] += $row->num; } else { $nameList[$given] = $row->num; } } } } arsort($nameList, SORT_NUMERIC); $nameList = array_slice($nameList, 0, $maxtoshow); if (count($nameList) == 0) { return ''; } if ($type == 'chart') { return $nameList; } $common = array(); foreach ($nameList as $given => $total) { if ($maxtoshow !== -1) { if ($maxtoshow-- <= 0) { break; } } if ($total < $threshold) { break; } if ($show_tot) { $tot = ' (' . I18N::number($total) . ')'; } else { $tot = ''; } switch ($type) { case 'table': $common[] = '' . $given . '' . I18N::number($total) . '' . $total . ''; break; case 'list': $common[] = '
  • ' . $given . '' . $tot . '
  • '; break; case 'nolist': $common[] = '' . $given . '' . $tot; break; } } if ($common) { switch ($type) { case 'table': global $controller; $table_id = Uuid::uuid4(); // lists requires a unique ID in case there are multiple lists per page $controller ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL) ->addInlineJavascript(' jQuery("#' . $table_id . '").dataTable({ dom: \'t\', autoWidth: false, paging: false, lengthChange: false, filter: false, info: false, jQueryUI: true, sorting: [[1,"desc"]], columns: [ /* 0-name */ {}, /* 1-count */ { class: "center", dataSort: 2}, /* 2-COUNT */ { visible: false} ] }); jQuery("#' . $table_id . '").css("visibility", "visible"); '); $lookup = array('M' => I18N::translate('Male'), 'F' => I18N::translate('Female'), 'U' => I18N::translateContext('unknown gender', 'Unknown'), 'B' => I18N::translate('All')); return '' . implode('', $common) . '
    ' . $lookup[$sex] . '
    ' . I18N::translate('Name') . '' . I18N::translate('Count') . 'COUNT
    '; case 'list': return ''; case 'nolist': return implode(I18N::$list_separator, $common); default: return ''; } } else { return ''; } } /** * Find common give names. * * @param string[] $params * * @return string */ public function commonGiven($params = array(1, 10, 'alpha')) { return $this->commonGivenQuery('B', 'nolist', false, $params); } /** * Find common give names. * * @param string[] $params * * @return string */ public function commonGivenTotals($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('B', 'nolist', true, $params); } /** * Find common give names. * * @param string[] $params * * @return string */ public function commonGivenList($params = array(1, 10, 'alpha')) { return $this->commonGivenQuery('B', 'list', false, $params); } /** * Find common give names. * * @param string[] $params * * @return string */ public function commonGivenListTotals($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('B', 'list', true, $params); } /** * Find common give names. * * @param string[] $params * * @return string */ public function commonGivenTable($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('B', 'table', false, $params); } /** * Find common give names of females. * * @param string[] $params * * @return string */ public function commonGivenFemale($params = array(1, 10, 'alpha')) { return $this->commonGivenQuery('F', 'nolist', false, $params); } /** * Find common give names of females. * * @param string[] $params * * @return string */ public function commonGivenFemaleTotals($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('F', 'nolist', true, $params); } /** * Find common give names of females. * * @param string[] $params * * @return string */ public function commonGivenFemaleList($params = array(1, 10, 'alpha')) { return $this->commonGivenQuery('F', 'list', false, $params); } /** * Find common give names of females. * * @param string[] $params * * @return string */ public function commonGivenFemaleListTotals($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('F', 'list', true, $params); } /** * Find common give names of females. * * @param string[] $params * * @return string */ public function commonGivenFemaleTable($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('F', 'table', false, $params); } /** * Find common give names of males. * * @param string[] $params * * @return string */ public function commonGivenMale($params = array(1, 10, 'alpha')) { return $this->commonGivenQuery('M', 'nolist', false, $params); } /** * Find common give names of males. * * @param string[] $params * * @return string */ public function commonGivenMaleTotals($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('M', 'nolist', true, $params); } /** * Find common give names of males. * * @param string[] $params * * @return string */ public function commonGivenMaleList($params = array(1, 10, 'alpha')) { return $this->commonGivenQuery('M', 'list', false, $params); } /** * Find common give names of males. * * @param string[] $params * * @return string */ public function commonGivenMaleListTotals($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('M', 'list', true, $params); } /** * Find common give names of males. * * @param string[] $params * * @return string */ public function commonGivenMaleTable($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('M', 'table', false, $params); } /** * Find common give names of unknown sexes. * * @param string[] $params * * @return string */ public function commonGivenUnknown($params = array(1, 10, 'alpha')) { return $this->commonGivenQuery('U', 'nolist', false, $params); } /** * Find common give names of unknown sexes. * * @param string[] $params * * @return string */ public function commonGivenUnknownTotals($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('U', 'nolist', true, $params); } /** * Find common give names of unknown sexes. * * @param string[] $params * * @return string */ public function commonGivenUnknownList($params = array(1, 10, 'alpha')) { return $this->commonGivenQuery('U', 'list', false, $params); } /** * Find common give names of unknown sexes. * * @param string[] $params * * @return string */ public function commonGivenUnknownListTotals($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('U', 'list', true, $params); } /** * Find common give names of unknown sexes. * * @param string[] $params * * @return string */ public function commonGivenUnknownTable($params = array(1, 10, 'rcount')) { return $this->commonGivenQuery('U', 'table', false, $params); } /** * Create a chart of common given names. * * @param string[] $params * * @return string */ public function chartCommonGiven($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } if (isset($params[4]) && $params[4] != '') { $maxtoshow = strtolower($params[4]); } else { $maxtoshow = 7; } $sizes = explode('x', $size); $tot_indi = $this->totalIndividualsQuery(); $given = $this->commonGivenQuery('B', 'chart'); if (!is_array($given)) { return ''; } $given = array_slice($given, 0, $maxtoshow); if (count($given) <= 0) { return ''; } $tot = 0; foreach ($given as $count) { $tot += $count; } $chd = ''; $chl = array(); foreach ($given as $givn => $count) { if ($tot == 0) { $per = 0; } else { $per = round(100 * $count / $tot_indi, 0); } $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = $givn . ' - ' . I18N::number($count); } $per = round(100 * ($tot_indi - $tot) / $tot_indi, 0); $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot); $chart_title = implode(I18N::$list_separator, $chl); $chl = implode('|', $chl); return "\"""; } /** * Who is currently logged in? * * @param string $type * * @return string */ private function usersLoggedInQuery($type = 'nolist') { $content = ''; // List active users $NumAnonymous = 0; $loggedusers = array(); foreach (User::allLoggedIn() as $user) { if (Auth::isAdmin() || $user->getPreference('visibleonline')) { $loggedusers[] = $user; } else { $NumAnonymous++; } } $LoginUsers = count($loggedusers); if ($LoginUsers == 0 && $NumAnonymous == 0) { return I18N::translate('No signed-in and no anonymous users'); } if ($NumAnonymous > 0) { $content .= '' . I18N::plural('%s anonymous signed-in user', '%s anonymous signed-in users', $NumAnonymous, I18N::number($NumAnonymous)) . ''; } if ($LoginUsers > 0) { if ($NumAnonymous) { if ($type == 'list') { $content .= "

    "; } else { $content .= " " . I18N::translate('and') . " "; } } $content .= '' . I18N::plural('%s signed-in user', '%s signed-in users', $LoginUsers, I18N::number($LoginUsers)) . ''; if ($type == 'list') { $content .= ''; } return $content; } /** * NUmber of users who are currently logged in? * * @param string $type * * @return int */ private function usersLoggedInTotalQuery($type = 'all') { $anon = 0; $visible = 0; foreach (User::allLoggedIn() as $user) { if (Auth::isAdmin() || $user->getPreference('visibleonline')) { $visible++; } else { $anon++; } } if ($type == 'anon') { return $anon; } elseif ($type == 'visible') { return $visible; } else { return $visible + $anon; } } /** * Who is currently logged in? * * @return string */ public function usersLoggedIn() { return $this->usersLoggedInQuery('nolist'); } /** * Who is currently logged in? * * @return string */ public function usersLoggedInList() { return $this->usersLoggedInQuery('list'); } /** * Who is currently logged in? * * @return int */ public function usersLoggedInTotal() { return $this->usersLoggedInTotalQuery('all'); } /** * Which visitors are currently logged in? * * @return int */ public function usersLoggedInTotalAnon() { return $this->usersLoggedInTotalQuery('anon'); } /** * Which visitors are currently logged in? * * @return int */ public function usersLoggedInTotalVisible() { return $this->usersLoggedInTotalQuery('visible'); } /** * Get the current user's ID. * * @return null|string */ public function userId() { return Auth::id(); } /** * Get the current user's username. * * @param string[] $params * * @return string */ public function userName($params = array()) { if (Auth::check()) { return Filter::escapeHtml(Auth::user()->getUserName()); } elseif (isset($params[0]) && $params[0] != '') { // if #username:visitor# was specified, then "visitor" will be returned when the user is not logged in return Filter::escapeHtml($params[0]); } else { return ''; } } /** * Get the current user's full name. * * @return string */ public function userFullName() { return Auth::check() ? Auth::user()->getRealNameHtml() : ''; } /** * Get the newest registered user. * * @param string $type * @param string[] $params * * @return string */ private function getLatestUserData($type = 'userid', $params = array()) { static $user_id = null; if ($user_id === null) { $user = User::findLatestToRegister(); } else { $user = User::find($user_id); } switch ($type) { default: case 'userid': return $user->getUserId(); case 'username': return Filter::escapeHtml($user->getUserName()); case 'fullname': return $user->getRealNameHtml(); case 'regdate': if (is_array($params) && isset($params[0]) && $params[0] != '') { $datestamp = $params[0]; } else { $datestamp = I18N::dateFormat(); } return FunctionsDate::timestampToGedcomDate($user->getPreference('reg_timestamp'))->display(false, $datestamp); case 'regtime': if (is_array($params) && isset($params[0]) && $params[0] != '') { $datestamp = $params[0]; } else { $datestamp = str_replace('%', '', I18N::timeFormat()); } return date($datestamp, $user->getPreference('reg_timestamp')); case 'loggedin': if (is_array($params) && isset($params[0]) && $params[0] != '') { $yes = $params[0]; } else { $yes = I18N::translate('yes'); } if (is_array($params) && isset($params[1]) && $params[1] != '') { $no = $params[1]; } else { $no = I18N::translate('no'); } return Database::prepare("SELECT SQL_NO_CACHE 1 FROM `##session` WHERE user_id=? LIMIT 1")->execute(array($user->getUserId()))->fetchOne() ? $yes : $no; } } /** * Get the newest registered user's ID. * * @return string */ public function latestUserId() { return $this->getLatestUserData('userid'); } /** * Get the newest registered user's username. * * @return string */ public function latestUserName() { return $this->getLatestUserData('username'); } /** * Get the newest registered user's real name. * * @return string */ public function latestUserFullName() { return $this->getLatestUserData('fullname'); } /** * Get the date of the newest user registration. * * @param string[] $params * * @return string */ public function latestUserRegDate($params = array()) { return $this->getLatestUserData('regdate', $params); } /** * Find the timestamp of the latest user to register. * * @param string[] $params * * @return string */ public function latestUserRegTime($params = array()) { return $this->getLatestUserData('regtime', $params); } /** * Find the most recent user to log in. * * @param string[] $params * * @return string */ public function latestUserLoggedin($params = array()) { return $this->getLatestUserData('loggedin', $params); } /** * Create a link to contact the webmaster. * * @return string */ public function contactWebmaster() { $user_id = $this->tree->getPreference('WEBMASTER_USER_ID'); $user = User::find($user_id); if ($user) { return Theme::theme()->contactLink($user); } else { return $user_id; } } /** * Create a link to contact the genealogy contact. * * @return string */ public function contactGedcom() { $user_id = $this->tree->getPreference('CONTACT_USER_ID'); $user = User::find($user_id); if ($user) { return Theme::theme()->contactLink($user); } else { return $user_id; } } /** * What is the current date on the server? * * @return string */ public function serverDate() { return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP)->display(); } /** * What is the current time on the server (in 12 hour clock)? * * @return string */ public function serverTime() { return date('g:i a'); } /** * What is the current time on the server (in 24 hour clock)? * * @return string */ public function serverTime24() { return date('G:i'); } /** * What is the timezone of the server. * * @return string */ public function serverTimezone() { return date('T'); } /** * What is the client's date. * * @return string */ public function browserDate() { return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET)->display(); } /** * What is the client's timestamp. * * @return string */ public function browserTime() { return date(str_replace('%', '', I18N::timeFormat()), WT_TIMESTAMP + WT_TIMESTAMP_OFFSET); } /** * What is the browser's tiemzone. * * @return string */ public function browserTimezone() { return date('T', WT_TIMESTAMP + WT_TIMESTAMP_OFFSET); } /** * What is the current version of webtrees. * * @return string */ public function webtreesVersion() { return WT_VERSION; } /** * These functions provide access to hitcounter for use in the HTML block. * * @param string $page_name * @param string[] $params * * @return string */ private function hitCountQuery($page_name, $params) { if (is_array($params) && isset($params[0]) && $params[0] != '') { $page_parameter = $params[0]; } else { $page_parameter = ''; } if ($page_name === null) { // index.php?ctype=gedcom $page_name = 'index.php'; $page_parameter = 'gedcom:' . ($page_parameter ? Tree::findByName($page_parameter)->getTreeId() : $this->tree->getTreeId()); } elseif ($page_name == 'index.php') { // index.php?ctype=user $user = User::findByIdentifier($page_parameter); $page_parameter = 'user:' . ($user ? $user->getUserId() : Auth::id()); } else { // indi/fam/sour/etc. } return '' . I18N::digits(HitCounter::getCount($this->tree, $page_name, $page_parameter)) . ''; } /** * How many times has a page been viewed. * * @param string[] $params * * @return string */ public function hitCount($params = array()) { return $this->hitCountQuery(null, $params); } /** * How many times has a page been viewed. * * @param string[] $params * * @return string */ public function hitCountUser($params = array()) { return $this->hitCountQuery('index.php', $params); } /** * How many times has a page been viewed. * * @param string[] $params * * @return string */ public function hitCountIndi($params = array()) { return $this->hitCountQuery('individual.php', $params); } /** * How many times has a page been viewed. * * @param string[] $params * * @return string */ public function hitCountFam($params = array()) { return $this->hitCountQuery('family.php', $params); } /** * How many times has a page been viewed. * * @param string[] $params * * @return string */ public function hitCountSour($params = array()) { return $this->hitCountQuery('source.php', $params); } /** * How many times has a page been viewed. * * @param string[] $params * * @return string */ public function hitCountRepo($params = array()) { return $this->hitCountQuery('repo.php', $params); } /** * How many times has a page been viewed. * * @param string[] $params * * @return string */ public function hitCountNote($params = array()) { return $this->hitCountQuery('note.php', $params); } /** * How many times has a page been viewed. * * @param string[] $params * * @return string */ public function hitCountObje($params = array()) { return $this->hitCountQuery('mediaviewer.php', $params); } /** * Convert numbers to Google's custom encoding. * * @link http://bendodson.com/news/google-extended-encoding-made-easy * * @param int[] $a * * @return string */ private function arrayToExtendedEncoding($a) { $xencoding = WT_GOOGLE_CHART_ENCODING; $encoding = ''; foreach ($a as $value) { if ($value < 0) { $value = 0; } $first = (int) ($value / 64); $second = $value % 64; $encoding .= $xencoding[(int) $first] . $xencoding[(int) $second]; } return $encoding; } /** * Callback function to compare totals. * * @param array $a * @param array $b * * @return int */ private function nameTotalSort($a, $b) { return $a['match'] - $b['match']; } /** * Callback function to compare totals. * * @param array $a * @param array $b * * @return int */ private function nameTotalReverseSort($a, $b) { return $b['match'] - $a['match']; } /** * Run an SQL query and cache the result. * * @param string $sql * * @return string[][] */ private function runSql($sql) { static $cache = array(); $id = md5($sql); if (isset($cache[$id])) { return $cache[$id]; } $rows = Database::prepare($sql)->fetchAll(PDO::FETCH_ASSOC); $cache[$id] = $rows; return $rows; } /** * Find the favorites for the tree. * * @return string */ public function gedcomFavorites() { if (Module::getModuleByName('gedcom_favorites')) { $block = new FamilyTreeFavoritesModule(WT_MODULES_DIR . 'gedcom_favorites'); return $block->getBlock(0, false); } else { return ''; } } /** * Find the favorites for the user. * * @return string */ public function userFavorites() { if (Auth::check() && Module::getModuleByName('user_favorites')) { $block = new UserFavoritesModule(WT_MODULES_DIR . 'gedcom_favorites'); return $block->getBlock(0, false); } else { return ''; } } /** * Find the number of favorites for the tree. * * @return int */ public function totalGedcomFavorites() { if (Module::getModuleByName('gedcom_favorites')) { return count(FamilyTreeFavoritesModule::getFavorites($this->tree->getTreeId())); } else { return 0; } } /** * Find the number of favorites for the user. * * @return int */ public function totalUserFavorites() { if (Module::getModuleByName('user_favorites')) { return count(UserFavoritesModule::getFavorites(Auth::id())); } else { return 0; } } /** * Create any of the other blocks. * * Use as #callBlock:block_name# * * @param string[] $params * * @return string */ public function callBlock($params = array()) { global $ctype; if (isset($params[0]) && $params[0] != '') { $block = $params[0]; } else { return ''; } $all_blocks = array(); foreach (Module::getActiveBlocks($this->tree) as $name => $active_block) { if ($ctype == 'user' && $active_block->isUserBlock() || $ctype == 'gedcom' && $active_block->isGedcomBlock()) { $all_blocks[$name] = $active_block; } } if (!array_key_exists($block, $all_blocks) || $block == 'html') { return ''; } // Build the config array array_shift($params); $cfg = array(); foreach ($params as $config) { $bits = explode('=', $config); if (count($bits) < 2) { continue; } $v = array_shift($bits); $cfg[$v] = implode('=', $bits); } $block = $all_blocks[$block]; $block_id = Filter::getInteger('block_id'); $content = $block->getBlock($block_id, false, $cfg); return $content; } /** * How many messages in the user's inbox. * * @return string */ public function totalUserMessages() { $total = (int) Database::prepare("SELECT SQL_CACHE COUNT(*) FROM `##message` WHERE user_id = ?") ->execute(array(Auth::id())) ->fetchOne(); return I18N::number($total); } /** * How many blog entries exist for this user. * * @return string */ public function totalUserJournal() { try { $number = (int) Database::prepare("SELECT SQL_CACHE COUNT(*) FROM `##news` WHERE user_id = ?") ->execute(array(Auth::id())) ->fetchOne(); } catch (PDOException $ex) { // The module may not be installed, so the table may not exist. $number = 0; } return I18N::number($number); } /** * How many news items exist for this tree. * * @return string */ public function totalGedcomNews() { try { $number = (int) Database::prepare("SELECT SQL_CACHE COUNT(*) FROM `##news` WHERE gedcom_id = ?") ->execute(array($this->tree->getTreeId())) ->fetchOne(); } catch (PDOException $ex) { // The module may not be installed, so the table may not exist. $number = 0; } return I18N::number($number); } /** * ISO3166 3 letter codes, with their 2 letter equivalent. * NOTE: this is not 1:1. ENG/SCO/WAL/NIR => GB * NOTE: this also includes champman codes and others. Should it? * * @return string[] */ public function iso3166() { return array( 'ABW' => 'AW', 'AFG' => 'AF', 'AGO' => 'AO', 'AIA' => 'AI', 'ALA' => 'AX', 'ALB' => 'AL', 'AND' => 'AD', 'ARE' => 'AE', 'ARG' => 'AR', 'ARM' => 'AM', 'ASM' => 'AS', 'ATA' => 'AQ', 'ATF' => 'TF', 'ATG' => 'AG', 'AUS' => 'AU', 'AUT' => 'AT', 'AZE' => 'AZ', 'BDI' => 'BI', 'BEL' => 'BE', 'BEN' => 'BJ', 'BFA' => 'BF', 'BGD' => 'BD', 'BGR' => 'BG', 'BHR' => 'BH', 'BHS' => 'BS', 'BIH' => 'BA', 'BLR' => 'BY', 'BLZ' => 'BZ', 'BMU' => 'BM', 'BOL' => 'BO', 'BRA' => 'BR', 'BRB' => 'BB', 'BRN' => 'BN', 'BTN' => 'BT', 'BVT' => 'BV', 'BWA' => 'BW', 'CAF' => 'CF', 'CAN' => 'CA', 'CCK' => 'CC', 'CHE' => 'CH', 'CHL' => 'CL', 'CHN' => 'CN', 'CIV' => 'CI', 'CMR' => 'CM', 'COD' => 'CD', 'COG' => 'CG', 'COK' => 'CK', 'COL' => 'CO', 'COM' => 'KM', 'CPV' => 'CV', 'CRI' => 'CR', 'CUB' => 'CU', 'CXR' => 'CX', 'CYM' => 'KY', 'CYP' => 'CY', 'CZE' => 'CZ', 'DEU' => 'DE', 'DJI' => 'DJ', 'DMA' => 'DM', 'DNK' => 'DK', 'DOM' => 'DO', 'DZA' => 'DZ', 'ECU' => 'EC', 'EGY' => 'EG', 'ENG' => 'GB', 'ERI' => 'ER', 'ESH' => 'EH', 'ESP' => 'ES', 'EST' => 'EE', 'ETH' => 'ET', 'FIN' => 'FI', 'FJI' => 'FJ', 'FLK' => 'FK', 'FRA' => 'FR', 'FRO' => 'FO', 'FSM' => 'FM', 'GAB' => 'GA', 'GBR' => 'GB', 'GEO' => 'GE', 'GHA' => 'GH', 'GIB' => 'GI', 'GIN' => 'GN', 'GLP' => 'GP', 'GMB' => 'GM', 'GNB' => 'GW', 'GNQ' => 'GQ', 'GRC' => 'GR', 'GRD' => 'GD', 'GRL' => 'GL', 'GTM' => 'GT', 'GUF' => 'GF', 'GUM' => 'GU', 'GUY' => 'GY', 'HKG' => 'HK', 'HMD' => 'HM', 'HND' => 'HN', 'HRV' => 'HR', 'HTI' => 'HT', 'HUN' => 'HU', 'IDN' => 'ID', 'IND' => 'IN', 'IOT' => 'IO', 'IRL' => 'IE', 'IRN' => 'IR', 'IRQ' => 'IQ', 'ISL' => 'IS', 'ISR' => 'IL', 'ITA' => 'IT', 'JAM' => 'JM', 'JOR' => 'JO', 'JPN' => 'JA', 'KAZ' => 'KZ', 'KEN' => 'KE', 'KGZ' => 'KG', 'KHM' => 'KH', 'KIR' => 'KI', 'KNA' => 'KN', 'KOR' => 'KO', 'KWT' => 'KW', 'LAO' => 'LA', 'LBN' => 'LB', 'LBR' => 'LR', 'LBY' => 'LY', 'LCA' => 'LC', 'LIE' => 'LI', 'LKA' => 'LK', 'LSO' => 'LS', 'LTU' => 'LT', 'LUX' => 'LU', 'LVA' => 'LV', 'MAC' => 'MO', 'MAR' => 'MA', 'MCO' => 'MC', 'MDA' => 'MD', 'MDG' => 'MG', 'MDV' => 'MV', 'MEX' => 'MX', 'MHL' => 'MH', 'MKD' => 'MK', 'MLI' => 'ML', 'MLT' => 'MT', 'MMR' => 'MM', 'MNG' => 'MN', 'MNP' => 'MP', 'MNT' => 'ME', 'MOZ' => 'MZ', 'MRT' => 'MR', 'MSR' => 'MS', 'MTQ' => 'MQ', 'MUS' => 'MU', 'MWI' => 'MW', 'MYS' => 'MY', 'MYT' => 'YT', 'NAM' => 'NA', 'NCL' => 'NC', 'NER' => 'NE', 'NFK' => 'NF', 'NGA' => 'NG', 'NIC' => 'NI', 'NIR' => 'GB', 'NIU' => 'NU', 'NLD' => 'NL', 'NOR' => 'NO', 'NPL' => 'NP', 'NRU' => 'NR', 'NZL' => 'NZ', 'OMN' => 'OM', 'PAK' => 'PK', 'PAN' => 'PA', 'PCN' => 'PN', 'PER' => 'PE', 'PHL' => 'PH', 'PLW' => 'PW', 'PNG' => 'PG', 'POL' => 'PL', 'PRI' => 'PR', 'PRK' => 'KP', 'PRT' => 'PO', 'PRY' => 'PY', 'PSE' => 'PS', 'PYF' => 'PF', 'QAT' => 'QA', 'REU' => 'RE', 'ROM' => 'RO', 'RUS' => 'RU', 'RWA' => 'RW', 'SAU' => 'SA', 'SCT' => 'GB', 'SDN' => 'SD', 'SEN' => 'SN', 'SER' => 'RS', 'SGP' => 'SG', 'SGS' => 'GS', 'SHN' => 'SH', 'SJM' => 'SJ', 'SLB' => 'SB', 'SLE' => 'SL', 'SLV' => 'SV', 'SMR' => 'SM', 'SOM' => 'SO', 'SPM' => 'PM', 'STP' => 'ST', 'SUR' => 'SR', 'SVK' => 'SK', 'SVN' => 'SI', 'SWE' => 'SE', 'SWZ' => 'SZ', 'SYC' => 'SC', 'SYR' => 'SY', 'TCA' => 'TC', 'TCD' => 'TD', 'TGO' => 'TG', 'THA' => 'TH', 'TJK' => 'TJ', 'TKL' => 'TK', 'TKM' => 'TM', 'TLS' => 'TL', 'TON' => 'TO', 'TTO' => 'TT', 'TUN' => 'TN', 'TUR' => 'TR', 'TUV' => 'TV', 'TWN' => 'TW', 'TZA' => 'TZ', 'UGA' => 'UG', 'UKR' => 'UA', 'UMI' => 'UM', 'URY' => 'UY', 'USA' => 'US', 'UZB' => 'UZ', 'VAT' => 'VA', 'VCT' => 'VC', 'VEN' => 'VE', 'VGB' => 'VG', 'VIR' => 'VI', 'VNM' => 'VN', 'VUT' => 'VU', 'WLF' => 'WF', 'WLS' => 'GB', 'WSM' => 'WS', 'YEM' => 'YE', 'ZAF' => 'ZA', 'ZMB' => 'ZM', 'ZWE' => 'ZW', ); } /** * Country codes and names * * @return string[] */ public function getAllCountries() { return array( '???' => /* I18N: Name of a country or state */ I18N::translate('Unknown'), 'ABW' => /* I18N: Name of a country or state */ I18N::translate('Aruba'), 'AFG' => /* I18N: Name of a country or state */ I18N::translate('Afghanistan'), 'AGO' => /* I18N: Name of a country or state */ I18N::translate('Angola'), 'AIA' => /* I18N: Name of a country or state */ I18N::translate('Anguilla'), 'ALA' => /* I18N: Name of a country or state */ I18N::translate('Aland Islands'), 'ALB' => /* I18N: Name of a country or state */ I18N::translate('Albania'), 'AND' => /* I18N: Name of a country or state */ I18N::translate('Andorra'), 'ARE' => /* I18N: Name of a country or state */ I18N::translate('United Arab Emirates'), 'ARG' => /* I18N: Name of a country or state */ I18N::translate('Argentina'), 'ARM' => /* I18N: Name of a country or state */ I18N::translate('Armenia'), 'ASM' => /* I18N: Name of a country or state */ I18N::translate('American Samoa'), 'ATA' => /* I18N: Name of a country or state */ I18N::translate('Antarctica'), 'ATF' => /* I18N: Name of a country or state */ I18N::translate('French Southern Territories'), 'ATG' => /* I18N: Name of a country or state */ I18N::translate('Antigua and Barbuda'), 'AUS' => /* I18N: Name of a country or state */ I18N::translate('Australia'), 'AUT' => /* I18N: Name of a country or state */ I18N::translate('Austria'), 'AZE' => /* I18N: Name of a country or state */ I18N::translate('Azerbaijan'), 'AZR' => /* I18N: Name of a country or state */ I18N::translate('Azores'), 'BDI' => /* I18N: Name of a country or state */ I18N::translate('Burundi'), 'BEL' => /* I18N: Name of a country or state */ I18N::translate('Belgium'), 'BEN' => /* I18N: Name of a country or state */ I18N::translate('Benin'), // BES => Bonaire, Sint Eustatius and Saba 'BFA' => /* I18N: Name of a country or state */ I18N::translate('Burkina Faso'), 'BGD' => /* I18N: Name of a country or state */ I18N::translate('Bangladesh'), 'BGR' => /* I18N: Name of a country or state */ I18N::translate('Bulgaria'), 'BHR' => /* I18N: Name of a country or state */ I18N::translate('Bahrain'), 'BHS' => /* I18N: Name of a country or state */ I18N::translate('Bahamas'), 'BIH' => /* I18N: Name of a country or state */ I18N::translate('Bosnia and Herzegovina'), // BLM => Saint Barthélemy 'BLR' => /* I18N: Name of a country or state */ I18N::translate('Belarus'), 'BLZ' => /* I18N: Name of a country or state */ I18N::translate('Belize'), 'BMU' => /* I18N: Name of a country or state */ I18N::translate('Bermuda'), 'BOL' => /* I18N: Name of a country or state */ I18N::translate('Bolivia'), 'BRA' => /* I18N: Name of a country or state */ I18N::translate('Brazil'), 'BRB' => /* I18N: Name of a country or state */ I18N::translate('Barbados'), 'BRN' => /* I18N: Name of a country or state */ I18N::translate('Brunei Darussalam'), 'BTN' => /* I18N: Name of a country or state */ I18N::translate('Bhutan'), 'BVT' => /* I18N: Name of a country or state */ I18N::translate('Bouvet Island'), 'BWA' => /* I18N: Name of a country or state */ I18N::translate('Botswana'), 'CAF' => /* I18N: Name of a country or state */ I18N::translate('Central African Republic'), 'CAN' => /* I18N: Name of a country or state */ I18N::translate('Canada'), 'CCK' => /* I18N: Name of a country or state */ I18N::translate('Cocos (Keeling) Islands'), 'CHE' => /* I18N: Name of a country or state */ I18N::translate('Switzerland'), 'CHL' => /* I18N: Name of a country or state */ I18N::translate('Chile'), 'CHN' => /* I18N: Name of a country or state */ I18N::translate('China'), 'CIV' => /* I18N: Name of a country or state */ I18N::translate('Cote d’Ivoire'), 'CMR' => /* I18N: Name of a country or state */ I18N::translate('Cameroon'), 'COD' => /* I18N: Name of a country or state */ I18N::translate('Democratic Republic of the Congo'), 'COG' => /* I18N: Name of a country or state */ I18N::translate('Republic of the Congo'), 'COK' => /* I18N: Name of a country or state */ I18N::translate('Cook Islands'), 'COL' => /* I18N: Name of a country or state */ I18N::translate('Colombia'), 'COM' => /* I18N: Name of a country or state */ I18N::translate('Comoros'), 'CPV' => /* I18N: Name of a country or state */ I18N::translate('Cape Verde'), 'CRI' => /* I18N: Name of a country or state */ I18N::translate('Costa Rica'), 'CUB' => /* I18N: Name of a country or state */ I18N::translate('Cuba'), // CUW => Curaçao 'CXR' => /* I18N: Name of a country or state */ I18N::translate('Christmas Island'), 'CYM' => /* I18N: Name of a country or state */ I18N::translate('Cayman Islands'), 'CYP' => /* I18N: Name of a country or state */ I18N::translate('Cyprus'), 'CZE' => /* I18N: Name of a country or state */ I18N::translate('Czech Republic'), 'DEU' => /* I18N: Name of a country or state */ I18N::translate('Germany'), 'DJI' => /* I18N: Name of a country or state */ I18N::translate('Djibouti'), 'DMA' => /* I18N: Name of a country or state */ I18N::translate('Dominica'), 'DNK' => /* I18N: Name of a country or state */ I18N::translate('Denmark'), 'DOM' => /* I18N: Name of a country or state */ I18N::translate('Dominican Republic'), 'DZA' => /* I18N: Name of a country or state */ I18N::translate('Algeria'), 'ECU' => /* I18N: Name of a country or state */ I18N::translate('Ecuador'), 'EGY' => /* I18N: Name of a country or state */ I18N::translate('Egypt'), 'ENG' => /* I18N: Name of a country or state */ I18N::translate('England'), 'ERI' => /* I18N: Name of a country or state */ I18N::translate('Eritrea'), 'ESH' => /* I18N: Name of a country or state */ I18N::translate('Western Sahara'), 'ESP' => /* I18N: Name of a country or state */ I18N::translate('Spain'), 'EST' => /* I18N: Name of a country or state */ I18N::translate('Estonia'), 'ETH' => /* I18N: Name of a country or state */ I18N::translate('Ethiopia'), 'FIN' => /* I18N: Name of a country or state */ I18N::translate('Finland'), 'FJI' => /* I18N: Name of a country or state */ I18N::translate('Fiji'), 'FLD' => /* I18N: Name of a country or state */ I18N::translate('Flanders'), 'FLK' => /* I18N: Name of a country or state */ I18N::translate('Falkland Islands'), 'FRA' => /* I18N: Name of a country or state */ I18N::translate('France'), 'FRO' => /* I18N: Name of a country or state */ I18N::translate('Faroe Islands'), 'FSM' => /* I18N: Name of a country or state */ I18N::translate('Micronesia'), 'GAB' => /* I18N: Name of a country or state */ I18N::translate('Gabon'), 'GBR' => /* I18N: Name of a country or state */ I18N::translate('United Kingdom'), 'GEO' => /* I18N: Name of a country or state */ I18N::translate('Georgia'), 'GGY' => /* I18N: Name of a country or state */ I18N::translate('Guernsey'), 'GHA' => /* I18N: Name of a country or state */ I18N::translate('Ghana'), 'GIB' => /* I18N: Name of a country or state */ I18N::translate('Gibraltar'), 'GIN' => /* I18N: Name of a country or state */ I18N::translate('Guinea'), 'GLP' => /* I18N: Name of a country or state */ I18N::translate('Guadeloupe'), 'GMB' => /* I18N: Name of a country or state */ I18N::translate('Gambia'), 'GNB' => /* I18N: Name of a country or state */ I18N::translate('Guinea-Bissau'), 'GNQ' => /* I18N: Name of a country or state */ I18N::translate('Equatorial Guinea'), 'GRC' => /* I18N: Name of a country or state */ I18N::translate('Greece'), 'GRD' => /* I18N: Name of a country or state */ I18N::translate('Grenada'), 'GRL' => /* I18N: Name of a country or state */ I18N::translate('Greenland'), 'GTM' => /* I18N: Name of a country or state */ I18N::translate('Guatemala'), 'GUF' => /* I18N: Name of a country or state */ I18N::translate('French Guiana'), 'GUM' => /* I18N: Name of a country or state */ I18N::translate('Guam'), 'GUY' => /* I18N: Name of a country or state */ I18N::translate('Guyana'), 'HKG' => /* I18N: Name of a country or state */ I18N::translate('Hong Kong'), 'HMD' => /* I18N: Name of a country or state */ I18N::translate('Heard Island and McDonald Islands'), 'HND' => /* I18N: Name of a country or state */ I18N::translate('Honduras'), 'HRV' => /* I18N: Name of a country or state */ I18N::translate('Croatia'), 'HTI' => /* I18N: Name of a country or state */ I18N::translate('Haiti'), 'HUN' => /* I18N: Name of a country or state */ I18N::translate('Hungary'), 'IDN' => /* I18N: Name of a country or state */ I18N::translate('Indonesia'), 'IND' => /* I18N: Name of a country or state */ I18N::translate('India'), 'IOM' => /* I18N: Name of a country or state */ I18N::translate('Isle of Man'), 'IOT' => /* I18N: Name of a country or state */ I18N::translate('British Indian Ocean Territory'), 'IRL' => /* I18N: Name of a country or state */ I18N::translate('Ireland'), 'IRN' => /* I18N: Name of a country or state */ I18N::translate('Iran'), 'IRQ' => /* I18N: Name of a country or state */ I18N::translate('Iraq'), 'ISL' => /* I18N: Name of a country or state */ I18N::translate('Iceland'), 'ISR' => /* I18N: Name of a country or state */ I18N::translate('Israel'), 'ITA' => /* I18N: Name of a country or state */ I18N::translate('Italy'), 'JAM' => /* I18N: Name of a country or state */ I18N::translate('Jamaica'), //'JEY' => Jersey 'JOR' => /* I18N: Name of a country or state */ I18N::translate('Jordan'), 'JPN' => /* I18N: Name of a country or state */ I18N::translate('Japan'), 'KAZ' => /* I18N: Name of a country or state */ I18N::translate('Kazakhstan'), 'KEN' => /* I18N: Name of a country or state */ I18N::translate('Kenya'), 'KGZ' => /* I18N: Name of a country or state */ I18N::translate('Kyrgyzstan'), 'KHM' => /* I18N: Name of a country or state */ I18N::translate('Cambodia'), 'KIR' => /* I18N: Name of a country or state */ I18N::translate('Kiribati'), 'KNA' => /* I18N: Name of a country or state */ I18N::translate('Saint Kitts and Nevis'), 'KOR' => /* I18N: Name of a country or state */ I18N::translate('Korea'), 'KWT' => /* I18N: Name of a country or state */ I18N::translate('Kuwait'), 'LAO' => /* I18N: Name of a country or state */ I18N::translate('Laos'), 'LBN' => /* I18N: Name of a country or state */ I18N::translate('Lebanon'), 'LBR' => /* I18N: Name of a country or state */ I18N::translate('Liberia'), 'LBY' => /* I18N: Name of a country or state */ I18N::translate('Libya'), 'LCA' => /* I18N: Name of a country or state */ I18N::translate('Saint Lucia'), 'LIE' => /* I18N: Name of a country or state */ I18N::translate('Liechtenstein'), 'LKA' => /* I18N: Name of a country or state */ I18N::translate('Sri Lanka'), 'LSO' => /* I18N: Name of a country or state */ I18N::translate('Lesotho'), 'LTU' => /* I18N: Name of a country or state */ I18N::translate('Lithuania'), 'LUX' => /* I18N: Name of a country or state */ I18N::translate('Luxembourg'), 'LVA' => /* I18N: Name of a country or state */ I18N::translate('Latvia'), 'MAC' => /* I18N: Name of a country or state */ I18N::translate('Macau'), // MAF => Saint Martin 'MAR' => /* I18N: Name of a country or state */ I18N::translate('Morocco'), 'MCO' => /* I18N: Name of a country or state */ I18N::translate('Monaco'), 'MDA' => /* I18N: Name of a country or state */ I18N::translate('Moldova'), 'MDG' => /* I18N: Name of a country or state */ I18N::translate('Madagascar'), 'MDV' => /* I18N: Name of a country or state */ I18N::translate('Maldives'), 'MEX' => /* I18N: Name of a country or state */ I18N::translate('Mexico'), 'MHL' => /* I18N: Name of a country or state */ I18N::translate('Marshall Islands'), 'MKD' => /* I18N: Name of a country or state */ I18N::translate('Macedonia'), 'MLI' => /* I18N: Name of a country or state */ I18N::translate('Mali'), 'MLT' => /* I18N: Name of a country or state */ I18N::translate('Malta'), 'MMR' => /* I18N: Name of a country or state */ I18N::translate('Myanmar'), 'MNG' => /* I18N: Name of a country or state */ I18N::translate('Mongolia'), 'MNP' => /* I18N: Name of a country or state */ I18N::translate('Northern Mariana Islands'), 'MNT' => /* I18N: Name of a country or state */ I18N::translate('Montenegro'), 'MOZ' => /* I18N: Name of a country or state */ I18N::translate('Mozambique'), 'MRT' => /* I18N: Name of a country or state */ I18N::translate('Mauritania'), 'MSR' => /* I18N: Name of a country or state */ I18N::translate('Montserrat'), 'MTQ' => /* I18N: Name of a country or state */ I18N::translate('Martinique'), 'MUS' => /* I18N: Name of a country or state */ I18N::translate('Mauritius'), 'MWI' => /* I18N: Name of a country or state */ I18N::translate('Malawi'), 'MYS' => /* I18N: Name of a country or state */ I18N::translate('Malaysia'), 'MYT' => /* I18N: Name of a country or state */ I18N::translate('Mayotte'), 'NAM' => /* I18N: Name of a country or state */ I18N::translate('Namibia'), 'NCL' => /* I18N: Name of a country or state */ I18N::translate('New Caledonia'), 'NER' => /* I18N: Name of a country or state */ I18N::translate('Niger'), 'NFK' => /* I18N: Name of a country or state */ I18N::translate('Norfolk Island'), 'NGA' => /* I18N: Name of a country or state */ I18N::translate('Nigeria'), 'NIC' => /* I18N: Name of a country or state */ I18N::translate('Nicaragua'), 'NIR' => /* I18N: Name of a country or state */ I18N::translate('Northern Ireland'), 'NIU' => /* I18N: Name of a country or state */ I18N::translate('Niue'), 'NLD' => /* I18N: Name of a country or state */ I18N::translate('Netherlands'), 'NOR' => /* I18N: Name of a country or state */ I18N::translate('Norway'), 'NPL' => /* I18N: Name of a country or state */ I18N::translate('Nepal'), 'NRU' => /* I18N: Name of a country or state */ I18N::translate('Nauru'), 'NZL' => /* I18N: Name of a country or state */ I18N::translate('New Zealand'), 'OMN' => /* I18N: Name of a country or state */ I18N::translate('Oman'), 'PAK' => /* I18N: Name of a country or state */ I18N::translate('Pakistan'), 'PAN' => /* I18N: Name of a country or state */ I18N::translate('Panama'), 'PCN' => /* I18N: Name of a country or state */ I18N::translate('Pitcairn'), 'PER' => /* I18N: Name of a country or state */ I18N::translate('Peru'), 'PHL' => /* I18N: Name of a country or state */ I18N::translate('Philippines'), 'PLW' => /* I18N: Name of a country or state */ I18N::translate('Palau'), 'PNG' => /* I18N: Name of a country or state */ I18N::translate('Papua New Guinea'), 'POL' => /* I18N: Name of a country or state */ I18N::translate('Poland'), 'PRI' => /* I18N: Name of a country or state */ I18N::translate('Puerto Rico'), 'PRK' => /* I18N: Name of a country or state */ I18N::translate('North Korea'), 'PRT' => /* I18N: Name of a country or state */ I18N::translate('Portugal'), 'PRY' => /* I18N: Name of a country or state */ I18N::translate('Paraguay'), 'PSE' => /* I18N: Name of a country or state */ I18N::translate('Occupied Palestinian Territory'), 'PYF' => /* I18N: Name of a country or state */ I18N::translate('French Polynesia'), 'QAT' => /* I18N: Name of a country or state */ I18N::translate('Qatar'), 'REU' => /* I18N: Name of a country or state */ I18N::translate('Reunion'), 'ROM' => /* I18N: Name of a country or state */ I18N::translate('Romania'), 'RUS' => /* I18N: Name of a country or state */ I18N::translate('Russia'), 'RWA' => /* I18N: Name of a country or state */ I18N::translate('Rwanda'), 'SAU' => /* I18N: Name of a country or state */ I18N::translate('Saudi Arabia'), 'SCT' => /* I18N: Name of a country or state */ I18N::translate('Scotland'), 'SDN' => /* I18N: Name of a country or state */ I18N::translate('Sudan'), 'SEA' => /* I18N: Name of a country or state */ I18N::translate('At sea'), 'SEN' => /* I18N: Name of a country or state */ I18N::translate('Senegal'), 'SER' => /* I18N: Name of a country or state */ I18N::translate('Serbia'), 'SGP' => /* I18N: Name of a country or state */ I18N::translate('Singapore'), 'SGS' => /* I18N: Name of a country or state */ I18N::translate('South Georgia and the South Sandwich Islands'), 'SHN' => /* I18N: Name of a country or state */ I18N::translate('Saint Helena'), 'SJM' => /* I18N: Name of a country or state */ I18N::translate('Svalbard and Jan Mayen'), 'SLB' => /* I18N: Name of a country or state */ I18N::translate('Solomon Islands'), 'SLE' => /* I18N: Name of a country or state */ I18N::translate('Sierra Leone'), 'SLV' => /* I18N: Name of a country or state */ I18N::translate('El Salvador'), 'SMR' => /* I18N: Name of a country or state */ I18N::translate('San Marino'), 'SOM' => /* I18N: Name of a country or state */ I18N::translate('Somalia'), 'SPM' => /* I18N: Name of a country or state */ I18N::translate('Saint Pierre and Miquelon'), 'SSD' => /* I18N: Name of a country or state */ I18N::translate('South Sudan'), 'STP' => /* I18N: Name of a country or state */ I18N::translate('Sao Tome and Principe'), 'SUR' => /* I18N: Name of a country or state */ I18N::translate('Suriname'), 'SVK' => /* I18N: Name of a country or state */ I18N::translate('Slovakia'), 'SVN' => /* I18N: Name of a country or state */ I18N::translate('Slovenia'), 'SWE' => /* I18N: Name of a country or state */ I18N::translate('Sweden'), 'SWZ' => /* I18N: Name of a country or state */ I18N::translate('Swaziland'), // SXM => Sint Maarten 'SYC' => /* I18N: Name of a country or state */ I18N::translate('Seychelles'), 'SYR' => /* I18N: Name of a country or state */ I18N::translate('Syria'), 'TCA' => /* I18N: Name of a country or state */ I18N::translate('Turks and Caicos Islands'), 'TCD' => /* I18N: Name of a country or state */ I18N::translate('Chad'), 'TGO' => /* I18N: Name of a country or state */ I18N::translate('Togo'), 'THA' => /* I18N: Name of a country or state */ I18N::translate('Thailand'), 'TJK' => /* I18N: Name of a country or state */ I18N::translate('Tajikistan'), 'TKL' => /* I18N: Name of a country or state */ I18N::translate('Tokelau'), 'TKM' => /* I18N: Name of a country or state */ I18N::translate('Turkmenistan'), 'TLS' => /* I18N: Name of a country or state */ I18N::translate('Timor-Leste'), 'TON' => /* I18N: Name of a country or state */ I18N::translate('Tonga'), 'TTO' => /* I18N: Name of a country or state */ I18N::translate('Trinidad and Tobago'), 'TUN' => /* I18N: Name of a country or state */ I18N::translate('Tunisia'), 'TUR' => /* I18N: Name of a country or state */ I18N::translate('Turkey'), 'TUV' => /* I18N: Name of a country or state */ I18N::translate('Tuvalu'), 'TWN' => /* I18N: Name of a country or state */ I18N::translate('Taiwan'), 'TZA' => /* I18N: Name of a country or state */ I18N::translate('Tanzania'), 'UGA' => /* I18N: Name of a country or state */ I18N::translate('Uganda'), 'UKR' => /* I18N: Name of a country or state */ I18N::translate('Ukraine'), 'UMI' => /* I18N: Name of a country or state */ I18N::translate('US Minor Outlying Islands'), 'URY' => /* I18N: Name of a country or state */ I18N::translate('Uruguay'), 'USA' => /* I18N: Name of a country or state */ I18N::translate('United States'), 'UZB' => /* I18N: Name of a country or state */ I18N::translate('Uzbekistan'), 'VAT' => /* I18N: Name of a country or state */ I18N::translate('Vatican City'), 'VCT' => /* I18N: Name of a country or state */ I18N::translate('Saint Vincent and the Grenadines'), 'VEN' => /* I18N: Name of a country or state */ I18N::translate('Venezuela'), 'VGB' => /* I18N: Name of a country or state */ I18N::translate('British Virgin Islands'), 'VIR' => /* I18N: Name of a country or state */ I18N::translate('US Virgin Islands'), 'VNM' => /* I18N: Name of a country or state */ I18N::translate('Vietnam'), 'VUT' => /* I18N: Name of a country or state */ I18N::translate('Vanuatu'), 'WLF' => /* I18N: Name of a country or state */ I18N::translate('Wallis and Futuna'), 'WLS' => /* I18N: Name of a country or state */ I18N::translate('Wales'), 'WSM' => /* I18N: Name of a country or state */ I18N::translate('Samoa'), 'YEM' => /* I18N: Name of a country or state */ I18N::translate('Yemen'), 'ZAF' => /* I18N: Name of a country or state */ I18N::translate('South Africa'), 'ZMB' => /* I18N: Name of a country or state */ I18N::translate('Zambia'), 'ZWE' => /* I18N: Name of a country or state */ I18N::translate('Zimbabwe'), ); } /** * Century name, English => 21st, Polish => XXI, etc. * * @param int $century * * @return string */ private function centuryName($century) { if ($century < 0) { return str_replace(-$century, self::centuryName(-$century), /* I18N: BCE=Before the Common Era, for Julian years < 0. See http://en.wikipedia.org/wiki/Common_Era */ I18N::translate('%s BCE', I18N::number(-$century))); } // The current chart engine (Google charts) can't handle markup switch ($century) { case 21: return strip_tags(I18N::translateContext('CENTURY', '21st')); case 20: return strip_tags(I18N::translateContext('CENTURY', '20th')); case 19: return strip_tags(I18N::translateContext('CENTURY', '19th')); case 18: return strip_tags(I18N::translateContext('CENTURY', '18th')); case 17: return strip_tags(I18N::translateContext('CENTURY', '17th')); case 16: return strip_tags(I18N::translateContext('CENTURY', '16th')); case 15: return strip_tags(I18N::translateContext('CENTURY', '15th')); case 14: return strip_tags(I18N::translateContext('CENTURY', '14th')); case 13: return strip_tags(I18N::translateContext('CENTURY', '13th')); case 12: return strip_tags(I18N::translateContext('CENTURY', '12th')); case 11: return strip_tags(I18N::translateContext('CENTURY', '11th')); case 10: return strip_tags(I18N::translateContext('CENTURY', '10th')); case 9: return strip_tags(I18N::translateContext('CENTURY', '9th')); case 8: return strip_tags(I18N::translateContext('CENTURY', '8th')); case 7: return strip_tags(I18N::translateContext('CENTURY', '7th')); case 6: return strip_tags(I18N::translateContext('CENTURY', '6th')); case 5: return strip_tags(I18N::translateContext('CENTURY', '5th')); case 4: return strip_tags(I18N::translateContext('CENTURY', '4th')); case 3: return strip_tags(I18N::translateContext('CENTURY', '3rd')); case 2: return strip_tags(I18N::translateContext('CENTURY', '2nd')); case 1: return strip_tags(I18N::translateContext('CENTURY', '1st')); default: return ($century - 1) . '01-' . $century . '00'; } } }